├── .coveragerc ├── requirements.txt ├── benchmark ├── telco-bench.b ├── telco_fractions.py └── microbench.py ├── MANIFEST.in ├── .gitignore ├── .github ├── dependabot.yml └── workflows │ ├── wheels.yml │ └── ci.yml ├── tox.ini ├── .travis.yml ├── pyproject.toml ├── README.rst ├── Makefile ├── dedup_wheels.py ├── appveyor.yml ├── setup.py ├── appveyor_env.cmd ├── CHANGES.rst ├── src ├── formatfloat_testcases.txt └── quicktions.pyx └── LICENSE /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | plugins = Cython.Coverage 3 | source = src 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | tox 2 | pytest 3 | coverage 4 | Cython>=3.1.7 5 | codecov 6 | -------------------------------------------------------------------------------- /benchmark/telco-bench.b: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scoder/quicktions/HEAD/benchmark/telco-bench.b -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include MANIFEST.in LICENSE *.rst 2 | include setup.py *.yml tox.ini *.cmd *.txt .coveragerc 3 | recursive-include src *.py *.pyx *.pxd *.c *.txt *.html 4 | recursive-include benchmark *.py telco-bench.b 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | __pycache__ 4 | *.so 5 | *.o 6 | 7 | *.egg 8 | *.egg-info 9 | 10 | /venv 11 | /build 12 | /dist 13 | /wheel*/ 14 | src/*.c 15 | src/*.html 16 | MANIFEST 17 | 18 | .tox 19 | .coverage 20 | .idea 21 | callgrind.* 22 | perf*.data 23 | *.patch 24 | *.orig 25 | *.rej 26 | *.dep 27 | *.swp 28 | *~ 29 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Maintain dependencies for GitHub Actions 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | # Check for updates to GitHub Actions every week 8 | interval: "weekly" 9 | groups: 10 | github-actions: 11 | patterns: 12 | - "*" 13 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py{38,39,310,311,312,313,314} 3 | 4 | [testenv] 5 | platform = 6 | windows: win32 7 | linux: linux 8 | darwin: darwin 9 | deps = 10 | Cython==3.1.7 11 | pytest 12 | #pytest-cov 13 | #coverage 14 | passenv = * 15 | setenv = 16 | #WITH_COVERAGE=1 17 | commands = 18 | python -m pytest -v src/test_fractions.py --capture=no --strict {posargs} 19 | # coverage run --parallel-mode --source=src -m pytest src/test_fractions.py --capture=no --strict {posargs} 20 | # coverage combine 21 | # coverage report -m --include=src/test_fractions.py #--include=src/quicktions.pyx 22 | # {windows,linux}: codecov 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | #env: 2 | # global: 3 | # - TWINE_USERNAME=scoder 4 | # # Note: TWINE_PASSWORD is set in Travis settings 5 | 6 | os: linux 7 | sudo: false 8 | 9 | language: python 10 | 11 | install: 12 | - pip install -r requirements-appveyor.txt tox-travis 13 | - python setup.py build_ext --inplace --with-cython 14 | 15 | script: 16 | - tox 17 | - python setup.py sdist bdist_wheel 18 | # the following is stolen from https://github.com/joerick/pyinstrument_cext/blob/master/.travis.yml 19 | # uncomment to push wheels automatically to pypi for tagged releases only (requires TWINE_PASSWORD to be set) 20 | # - | 21 | # if [[ $TRAVIS_TAG ]]; then 22 | # python -m pip install twine 23 | # python -m twine upload wheelhouse/*.whl 24 | # fi 25 | 26 | matrix: 27 | include: 28 | - python: 2.7 29 | - python: 3.9 30 | - python: 3.8 31 | - python: 3.7 32 | - python: 3.5 33 | - python: 3.6 34 | #- python: 3.9-dev 35 | # - os: osx 36 | # osx_image: xcode6.4 37 | # env: PY=2 38 | # python: 2 39 | # language: c 40 | # compiler: clang 41 | # cache: false 42 | # - os: osx 43 | # osx_image: xcode6.4 44 | # env: PY=3 45 | # python: 3 46 | # language: c 47 | # compiler: clang 48 | # cache: false 49 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["Cython>=3.1.7", "setuptools"] 3 | 4 | [tool.cibuildwheel] 5 | build-verbosity = 2 6 | skip = ["pp*", "*-musllinux_i686", "cp35*", "cp36*", "cp37*"] 7 | enable =["cpython-freethreading", "cpython-prerelease"] 8 | # test-command = "python -m unittest {package}/test_fractions.py -p -v" 9 | 10 | [tool.cibuildwheel.linux] 11 | archs = ["x86_64", "aarch64", "i686", "ppc64le", "armv7l", "riscv64"] 12 | repair-wheel-command = "auditwheel repair --strip -w {dest_dir} {wheel}" 13 | 14 | [tool.cibuildwheel.linux.environment] 15 | CFLAGS = "-O3 -g1 -pipe -fPIC" 16 | AR = "gcc-ar" 17 | NM = "gcc-nm" 18 | RANLIB = "gcc-ranlib" 19 | 20 | [[tool.cibuildwheel.overrides]] 21 | select = "*linux_i686" 22 | inherit.environment = "append" 23 | environment.CFLAGS = "-O3 -g1 -pipe -fPIC -march=core2 -mtune=generic -mno-ssse3" 24 | 25 | [[tool.cibuildwheel.overrides]] 26 | select = "*linux_x86_64" 27 | inherit.environment = "append" 28 | environment.CFLAGS = "-O3 -g1 -pipe -fPIC -march=core2 -mtune=generic -mno-ssse3" 29 | 30 | [[tool.cibuildwheel.overrides]] 31 | select = "*aarch64" 32 | inherit.environment = "append" 33 | environment.CFLAGS = "-O3 -g1 -pipe -fPIC -march=armv8-a -mtune=cortex-a72" 34 | 35 | [tool.cibuildwheel.windows] 36 | archs = ["AMD64", "x86"] 37 | 38 | [tool.cibuildwheel.macos] 39 | # https://cibuildwheel.readthedocs.io/en/stable/faq/#what-to-provide suggests to provide 40 | # x86_64 and one of universal2 or arm64 wheels. x86_64 is still required by older pips, 41 | # so additional arm64 wheels suffice. 42 | #archs = ["x86_64", "universal2"] 43 | archs = ["x86_64", "arm64"] 44 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | quicktions 3 | ========== 4 | 5 | Python's ``Fraction`` data type is an excellent way to do exact calculations 6 | with unlimited rational numbers and largely beats ``Decimal`` in terms of 7 | simplicity, accuracy and safety. Clearly not in terms of speed, though, 8 | given the cdecimal accelerator in Python 3.3+. 9 | 10 | ``quicktions`` is an adaptation of the original ``fractions`` module 11 | (as included in CPython 3.13) that is compiled and optimised with 12 | `Cython `_ into a fast, native extension module. 13 | 14 | Compared to the standard library ``fractions`` module of CPython 3.12, 15 | computations in ``quicktions`` are about 2-4x faster. 16 | 17 | Instantiation of a ``Fraction`` in ``quicktions`` is also 18 | 19 | - 5-15x faster from a floating point string value (e.g. ``Fraction("123.456789")``) 20 | - 3-5x faster from a floating point value (e.g. ``Fraction(123.456789)``) 21 | - 2-5x faster from an integer numerator-denominator pair (e.g. ``Fraction(123, 456)``) 22 | 23 | We provide a set of micro-benchmarks here: 24 | 25 | https://github.com/scoder/quicktions/tree/master/benchmark 26 | 27 | As of quicktions 1.19, the different number types and implementations compare 28 | as follows in CPython 3.12 (measured on Ubuntu Linux): 29 | 30 | .. code-block:: 31 | 32 | Average times for all 'create' benchmarks: 33 | float : 19.69 us (1.0x) 34 | Fraction : 58.63 us (3.0x) 35 | Decimal : 84.32 us (4.3x) 36 | PyFraction : 208.20 us (10.6x) 37 | 38 | Average times for all 'compute' benchmarks: 39 | float : 1.79 us (1.0x) 40 | Decimal : 10.11 us (5.7x) 41 | Fraction : 39.24 us (22.0x) 42 | PyFraction : 96.23 us (53.9x) 43 | 44 | While not as fast as the C implemented ``decimal`` module in Python 3, 45 | ``quicktions`` is about 15x faster than the Python implemented ``decimal`` 46 | module in Python 2.7. 47 | 48 | For documentation, see the Python standard library's ``fractions`` module: 49 | 50 | https://docs.python.org/3/library/fractions.html 51 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PYTHON?=python3 2 | VERSION?=$(shell sed -ne "s|^__version__\s*=\s*'\([^']*\)'.*|\1|p" src/quicktions.pyx) 3 | PACKAGE=quicktions 4 | WITH_CYTHON := $(shell $(PYTHON) -c 'from Cython.Build import cythonize' 2>/dev/null && echo "--with-cython") 5 | PYTHON_WHEEL_BUILD_VERSION := "cp*" 6 | 7 | MANYLINUX_IMAGES= \ 8 | manylinux1_x86_64 \ 9 | manylinux1_i686 \ 10 | manylinux_2_24_x86_64 \ 11 | manylinux_2_24_i686 \ 12 | manylinux_2_24_aarch64 \ 13 | manylinux_2_28_x86_64 \ 14 | manylinux_2_34_x86_64 \ 15 | manylinux_2_28_aarch64 \ 16 | manylinux_2_34_aarch64 \ 17 | musllinux_1_1_x86_64 \ 18 | musllinux_1_2_x86_64 19 | 20 | .PHONY: all local sdist test clean realclean wheel_manylinux 21 | 22 | all: local 23 | 24 | local: 25 | ${PYTHON} setup.py build_ext --inplace $(WITH_CYTHON) 26 | 27 | sdist: dist/$(PACKAGE)-$(VERSION).tar.gz 28 | 29 | dist/$(PACKAGE)-$(VERSION).tar.gz: 30 | $(PYTHON) setup.py sdist $(WITH_CYTHON) 31 | 32 | testslow: local 33 | PYTHONPATH=src $(PYTHON) src/test_fractions.py 34 | 35 | test: local 36 | PYTHONPATH=src $(PYTHON) src/test_fractions.py --fast 37 | 38 | clean: 39 | rm -fr build src/*.so 40 | 41 | realclean: clean 42 | rm -fr src/*.c src/*.html 43 | 44 | qemu-user-static: 45 | docker run --rm --privileged hypriot/qemu-register 46 | 47 | wheel: 48 | $(PYTHON) setup.py bdist_wheel 49 | 50 | wheel_manylinux: sdist $(addprefix wheel_,$(MANYLINUX_IMAGES)) 51 | $(addprefix wheel_,$(filter-out %_x86_64, $(filter-out %_i686, $(MANYLINUX_IMAGES)))): qemu-user-static 52 | 53 | wheel_%: dist/$(PACKAGE)-$(VERSION).tar.gz 54 | echo "Building wheels for $(PACKAGE) $(VERSION)" 55 | time docker run --rm -t \ 56 | -v $(shell pwd):/io \ 57 | -e CFLAGS="-O3 -g0 -mtune=generic -pipe -fPIC" \ 58 | -e LDFLAGS="$(LDFLAGS) -fPIC" \ 59 | -e WHEELHOUSE=wheelhouse$(subst wheel_manylinux,,$@) \ 60 | quay.io/pypa/$(subst wheel_,,$@) \ 61 | bash -c 'for PYBIN in /opt/python/$(PYTHON_WHEEL_BUILD_VERSION)/bin; do \ 62 | $$PYBIN/python -V; \ 63 | { $$PYBIN/pip wheel -w /io/$$WHEELHOUSE /io/$< & } ; \ 64 | done; wait; \ 65 | for whl in /io/$$WHEELHOUSE/$(PACKAGE)-$(VERSION)-*-linux_*.whl; do auditwheel repair $$whl -w /io/$$WHEELHOUSE; done' 66 | -------------------------------------------------------------------------------- /dedup_wheels.py: -------------------------------------------------------------------------------- 1 | """ 2 | Split wheels by set of architectures and delete redundant ones. 3 | """ 4 | 5 | import argparse 6 | import os 7 | import pathlib 8 | import re 9 | import sys 10 | 11 | from collections import defaultdict 12 | from typing import Iterable 13 | 14 | 15 | def list_wheels(wheel_dir: pathlib.Path): 16 | return (wheel.name for wheel in wheel_dir.glob("*.whl")) 17 | 18 | 19 | def dedup_wheels(wheel_names: Iterable[str]): 20 | split_wheelname = re.compile(r"(?P\w+-[0-9a-z.]+)-(?P[^-]+-[^-]+)-(?P(?:[^.]+[.])+)whl").match 21 | 22 | keep: set = set() 23 | seen: dict[tuple[str,str],list[set[str]]] = defaultdict(list) 24 | best: dict[tuple[str,str],list[str]] = defaultdict(list) 25 | 26 | for wheel in sorted(wheel_names, key=len, reverse=True): 27 | parts = split_wheelname(wheel) 28 | if not parts: 29 | keep.add(wheel) 30 | continue 31 | 32 | archs = set(parts['archs'].rstrip('.').split('.')) 33 | key = (parts['project'], parts['python']) 34 | for archs_seen in seen[key]: 35 | if not (archs - archs_seen): 36 | break 37 | else: 38 | seen[key].append(archs) 39 | best[key].append(wheel) 40 | 41 | keep.update(wheel for wheel_list in best.values() for wheel in wheel_list) 42 | return sorted(keep) 43 | 44 | 45 | def print_wheels(wheel_names: Iterable[str]): 46 | for wheel in sorted(wheel_names): 47 | print(f" {wheel}") 48 | 49 | 50 | def main(wheel_dir: str, delete=True): 51 | wheel_path = pathlib.Path(wheel_dir) 52 | all_wheels = list(list_wheels(wheel_path)) 53 | wheels = dedup_wheels(all_wheels) 54 | redundant = set(all_wheels).difference(wheels) 55 | 56 | if delete: 57 | for wheel in sorted(redundant): 58 | print(f"deleting {wheel}") 59 | os.unlink(wheel_path / wheel) 60 | elif redundant: 61 | print("Redundant wheels found:") 62 | print_wheels(redundant) 63 | 64 | 65 | def parse_args(args): 66 | parser = argparse.ArgumentParser() 67 | parser.add_argument("directory") 68 | parser.add_argument("-d", "--delete", action="store_true") 69 | 70 | return parser.parse_args(args) 71 | 72 | 73 | if __name__ == "__main__": 74 | if len(sys.argv) > 1: 75 | args = parse_args(sys.argv[1:]) 76 | main(args.directory, delete=args.delete) 77 | else: 78 | print(f"Usage: {sys.argv[0]} WHEEL_DIR", file=sys.stderr) 79 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | image: Visual Studio 2019 2 | 3 | # parts from 4 | # - matrix: https://github.com/pythonnet/pythonnet/blob/master/appveyor.yml 5 | # - Visual Studio 2010 py33 py34 on x64: https://github.com/ogrisel/python-appveyor-demo/blob/master/appveyor/run_with_env.cmd 6 | 7 | branches: 8 | only: 9 | - /default/ 10 | 11 | platform: 12 | - x86 13 | - x64 14 | 15 | environment: 16 | global: 17 | # SDK v7.0 MSVC Express 2008's SetEnv.cmd script will fail if the 18 | # /E:ON and /V:ON options are not enabled in the batch script intepreter 19 | # See: http://stackoverflow.com/a/13751649/163740 20 | CMD_IN_ENV: 'cmd /E:ON /V:ON /C .\appveyor_env.cmd' 21 | 22 | # CIBW_BEFORE_BUILD: python build_ext --with-cython --inplace 23 | # TWINE_USERNAME: scoder 24 | # Note: TWINE_PASSWORD is set in Appveyor settings 25 | 26 | matrix: 27 | - PYTHON_VERSION: 2.7 28 | APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2013 29 | - PYTHON_VERSION: "3.11" 30 | - PYTHON_VERSION: "3.10" 31 | - PYTHON_VERSION: 3.9 32 | - PYTHON_VERSION: 3.8 33 | - PYTHON_VERSION: 3.7 34 | - PYTHON_VERSION: 3.6 35 | 36 | init: 37 | - set PY_VER=%PYTHON_VERSION:.=% 38 | - set TOXENV=py%PY_VER% 39 | - set PYTHON=C:\PYTHON%PY_VER% 40 | - if %PLATFORM%==x64 (set PYTHON=%PYTHON%-x64) 41 | - set TOXPYTHON=%PYTHON%\python.exe 42 | 43 | # Put desired Python version in PATH 44 | - set PATH=%PYTHON%;%PYTHON%\Scripts;%PATH% 45 | 46 | - 'ECHO %TOXENV% ' 47 | - python --version 48 | 49 | install: 50 | #- pip install --upgrade pip 51 | - pip install -r requirements.txt 52 | - python setup.py build_ext --inplace --with-cython 53 | 54 | build: false # First tests then build (is python not C) 55 | 56 | test_script: 57 | - tox -e %TOXENV%-windows 58 | 59 | after_test: 60 | - pip install --upgrade wheel 61 | - python setup.py build_ext --inplace 62 | - rm -rf build # account for: https://bitbucket.org/pypa/wheel/issues/147/bdist_wheel-should-start-by-cleaning-up 63 | - python setup.py sdist bdist_wheel 64 | 65 | artifacts: 66 | # bdist_wheel puts your built wheel in the dist directory 67 | - path: "dist\\*.whl" 68 | name: Wheels 69 | # Where in the appveyor cloud are the wheels pushed to? 70 | # https://www.appveyor.com/docs/packaging-artifacts/#permalink-to-the-last-successful-build-artifact 71 | # borrowed from https://github.com/joerick/pyinstrument_cext/blob/master/appveyor.yml#L11-L15 72 | # - ps: >- 73 | # if ($env:APPVEYOR_REPO_TAG -eq "true") { 74 | # python -m pip install twine 75 | # python -m twine upload (resolve-path wheelhouse\*.whl) 76 | # } 77 | 78 | on_failure: 79 | - ps: dir "env:" 80 | - ps: get-content .tox\*\log\* 81 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import re 4 | import sys 5 | 6 | from setuptools import setup, Extension 7 | 8 | 9 | ext_modules = [ 10 | Extension("quicktions", ["src/quicktions.pyx"]), 11 | ] 12 | 13 | try: 14 | sys.argv.remove("--with-profile") 15 | except ValueError: 16 | enable_profiling = False 17 | else: 18 | enable_profiling = True 19 | 20 | enable_coverage = os.environ.get("WITH_COVERAGE") == "1" 21 | force_rebuild = os.environ.get("FORCE_REBUILD") == "1" 22 | 23 | try: 24 | sys.argv.remove("--with-cython") 25 | except ValueError: 26 | pass # legacy option 27 | 28 | cythonize = None 29 | try: 30 | sys.argv.remove("--no-cython") 31 | except ValueError: 32 | try: 33 | from Cython.Build import cythonize 34 | from Cython import __version__ as cython_version 35 | import Cython.Compiler.Options as cython_options 36 | cython_options.annotate = True 37 | except ImportError: 38 | print("Cython not found, building without Cython") 39 | cythonize = None 40 | else: 41 | print("Building with Cython %s" % cython_version) 42 | compiler_directives = {} 43 | if enable_profiling: 44 | compiler_directives['profile'] = True 45 | if enable_coverage: 46 | compiler_directives['linetrace'] = True 47 | ext_modules = cythonize( 48 | ext_modules, compiler_directives=compiler_directives, force=force_rebuild) 49 | 50 | if cythonize is None: 51 | for ext_module in ext_modules: 52 | ext_module.sources[:] = [m.replace('.pyx', '.c') for m in ext_module.sources] 53 | elif enable_coverage: 54 | for ext_module in ext_modules: 55 | ext_module.extra_compile_args += [ 56 | "-DCYTHON_TRACE_NOGIL=1", 57 | ] 58 | 59 | if sys.platform == "darwin": 60 | try: 61 | if int(os.environ.get("MACOSX_DEPLOYMENT_TARGET", "0").split(".", 1)[0]) >= 11: 62 | if "-arch" not in os.environ.get("CFLAGS", ""): 63 | os.environ["CFLAGS"] = os.environ.get("CFLAGS", "") + " -arch arm64 -arch x86_64" 64 | os.environ["LDFLAGS"] = os.environ.get("LDFLAGS", "") + " -arch arm64 -arch x86_64" 65 | except ValueError: 66 | pass # probably cannot parse "MACOSX_DEPLOYMENT_TARGET" 67 | 68 | 69 | with open('src/quicktions.pyx') as f: 70 | version = re.search(r"__version__\s*=\s*'([^']+)'", f.read(2048)).group(1) 71 | 72 | with open('README.rst') as f: 73 | long_description = ''.join(f.readlines()[3:]).strip() 74 | 75 | with open('CHANGES.rst') as f: 76 | long_description += '\n\n' + f.read() 77 | 78 | 79 | setup( 80 | name="quicktions", 81 | version=version, 82 | description="Fast fractions data type for rational numbers. " 83 | "Cythonized version of 'fractions.Fraction'.", 84 | long_description=long_description, 85 | author="Stefan Behnel", 86 | author_email="stefan_ml@behnel.de", 87 | url="https://github.com/scoder/quicktions", 88 | #bugtrack_url="https://github.com/scoder/quicktions/issues", 89 | license="PSF-2.0", 90 | 91 | ext_modules=ext_modules, 92 | package_dir={'': 'src'}, 93 | 94 | classifiers=[ 95 | "Development Status :: 6 - Mature", 96 | "Intended Audience :: Developers", 97 | "Operating System :: OS Independent", 98 | "Programming Language :: Python", 99 | "Programming Language :: Python :: 3", 100 | "Programming Language :: Cython", 101 | "Topic :: Scientific/Engineering :: Mathematics", 102 | "Topic :: Office/Business :: Financial", 103 | ], 104 | ) 105 | -------------------------------------------------------------------------------- /appveyor_env.cmd: -------------------------------------------------------------------------------- 1 | :: To build extensions for 64 bit Python 3, we need to configure environment 2 | :: variables to use the MSVC 2010 C++ compilers from GRMSDKX_EN_DVD.iso of: 3 | :: MS Windows SDK for Windows 7 and .NET Framework 4 (SDK v7.1) 4 | :: 5 | :: To build extensions for 64 bit Python 2, we need to configure environment 6 | :: variables to use the MSVC 2008 C++ compilers from GRMSDKX_EN_DVD.iso of: 7 | :: MS Windows SDK for Windows 7 and .NET Framework 3.5 (SDK v7.0) 8 | :: 9 | :: 32 bit builds, and 64-bit builds for 3.5 and beyond, do not require specific 10 | :: environment configurations. 11 | :: 12 | :: Note: this script needs to be run with the /E:ON and /V:ON flags for the 13 | :: cmd interpreter, at least for (SDK v7.0) 14 | :: 15 | :: More details at: 16 | :: https://github.com/cython/cython/wiki/64BitCythonExtensionsOnWindows 17 | :: http://stackoverflow.com/a/13751649/163740 18 | :: 19 | :: Author: Olivier Grisel 20 | :: License: CC0 1.0 Universal: http://creativecommons.org/publicdomain/zero/1.0/ 21 | :: 22 | :: Notes about batch files for Python people: 23 | :: 24 | :: Quotes in values are literally part of the values: 25 | :: SET FOO="bar" 26 | :: FOO is now five characters long: " b a r " 27 | :: If you don't want quotes, don't include them on the right-hand side. 28 | :: 29 | :: The CALL lines at the end of this file look redundant, but if you move them 30 | :: outside of the IF clauses, they do not run properly in the SET_SDK_64==Y 31 | :: case, I don't know why. 32 | @ECHO OFF 33 | 34 | SET COMMAND_TO_RUN=%* 35 | SET WIN_SDK_ROOT=C:\Program Files\Microsoft SDKs\Windows 36 | SET WIN_WDK=c:\Program Files (x86)\Windows Kits\10\Include\wdf 37 | 38 | :: Extract the major and minor versions, and allow for the minor version to be 39 | :: more than 9. This requires the version number to have two dots in it. 40 | SET MAJOR_PYTHON_VERSION=%PYTHON_VERSION:~0,1% 41 | IF "%PYTHON_VERSION:~3,1%" == "." ( 42 | SET MINOR_PYTHON_VERSION=%PYTHON_VERSION:~2,1% 43 | ) ELSE ( 44 | SET MINOR_PYTHON_VERSION=%PYTHON_VERSION:~2,2% 45 | ) 46 | 47 | :: Based on the Python version, determine what SDK version to use, and whether 48 | :: to set the SDK for 64-bit. 49 | IF %MAJOR_PYTHON_VERSION% == 2 ( 50 | SET WINDOWS_SDK_VERSION="v7.0" 51 | SET SET_SDK_64=Y 52 | ) ELSE ( 53 | IF %MAJOR_PYTHON_VERSION% == 3 ( 54 | SET WINDOWS_SDK_VERSION="v7.1" 55 | IF %MINOR_PYTHON_VERSION% LEQ 4 ( 56 | SET SET_SDK_64=Y 57 | ) ELSE ( 58 | SET SET_SDK_64=N 59 | IF EXIST "%WIN_WDK%" ( 60 | :: See: https://connect.microsoft.com/VisualStudio/feedback/details/1610302/ 61 | REN "%WIN_WDK%" 0wdf 62 | ) 63 | ) 64 | ) ELSE ( 65 | ECHO Unsupported Python version: "%MAJOR_PYTHON_VERSION%" 66 | EXIT 1 67 | ) 68 | ) 69 | 70 | IF %PYTHON_ARCH% == 64 ( 71 | IF %SET_SDK_64% == Y ( 72 | ECHO Configuring Windows SDK %WINDOWS_SDK_VERSION% for Python %MAJOR_PYTHON_VERSION% on a 64 bit architecture 73 | SET DISTUTILS_USE_SDK=1 74 | SET MSSdk=1 75 | "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Setup\WindowsSdkVer.exe" -q -version:%WINDOWS_SDK_VERSION% 76 | "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Bin\SetEnv.cmd" /x64 /release 77 | ECHO Executing: %COMMAND_TO_RUN% 78 | call %COMMAND_TO_RUN% || EXIT 1 79 | ) ELSE ( 80 | ECHO Using default MSVC build environment for 64 bit architecture 81 | ECHO Executing: %COMMAND_TO_RUN% 82 | call %COMMAND_TO_RUN% || EXIT 1 83 | ) 84 | ) ELSE ( 85 | ECHO Using default MSVC build environment for 32 bit architecture 86 | ECHO Executing: %COMMAND_TO_RUN% 87 | call %COMMAND_TO_RUN% || EXIT 1 88 | ) 89 | -------------------------------------------------------------------------------- /benchmark/telco_fractions.py: -------------------------------------------------------------------------------- 1 | #-*- coding: UTF-8 -*- 2 | 3 | # The MIT License 4 | # 5 | # Permission is hereby granted, free of charge, to any person 6 | # obtaining a copy of this software and associated documentation 7 | # files (the "Software"), to deal in the Software without 8 | # restriction, including without limitation the rights to use, 9 | # copy, modify, merge, publish, distribute, sublicense, and/or 10 | # sell copies of the Software, and to permit persons to whom the 11 | # Software is furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included 14 | # in all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 19 | # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 22 | # DEALINGS IN THE SOFTWARE. 23 | 24 | 25 | # Adapted for fractions by Stefan Behnel. Original version from 26 | # https://hg.python.org/benchmarks/file/9a1136898539/performance/bm_telco.py 27 | 28 | 29 | """ Telco Benchmark for measuring the performance of Fraction calculations 30 | 31 | http://www2.hursley.ibm.com/decimal/telco.html 32 | http://www2.hursley.ibm.com/decimal/telcoSpec.html 33 | 34 | A call type indicator, c, is set from the bottom (least significant) bit of the duration (hence c is 0 or 1). 35 | A r, r, is determined from the call type. Those calls with c=0 have a low r: 0.0013; the remainder (‘distance calls’) have a ‘premium’ r: 0.00894. (The rates are, very roughly, in Euros or dollarates per second.) 36 | A price, p, for the call is then calculated (p=r*n). 37 | A basic tax, b, is calculated: b=p*0.0675 (6.75%), and the total basic tax variable is then incremented (sumB=sumB+b). 38 | For distance calls: a distance tax, d, is calculated: d=p*0.0341 (3.41%), and then the total distance tax variable is incremented (sumD=sumD+d). 39 | The total price, t, is calculated (t=p+b, and, if a distance call, t=t+d). 40 | The total prices variable is incremented (sumT=sumT+t). 41 | The total price, t, is converted to a string, s. 42 | """ 43 | 44 | import os 45 | from math import fsum 46 | from struct import unpack 47 | from time import time 48 | 49 | 50 | def rel_path(*path): 51 | return os.path.join(os.path.dirname(__file__), *path) 52 | 53 | try: 54 | from quicktions import Fraction 55 | except ImportError: 56 | import sys 57 | sys.path.insert(0, rel_path('..', 'src')) 58 | 59 | from quicktions import Fraction 60 | 61 | 62 | filename = rel_path("telco-bench.b") 63 | 64 | 65 | def run(cls): 66 | rates = list(map(cls, ('0.0013', '0.00894'))) 67 | basictax = cls("0.0675") 68 | disttax = cls("0.0341") 69 | 70 | values = [] 71 | with open(filename, "rb") as infil: 72 | for _ in range(20000): 73 | datum = infil.read(8) 74 | if datum == '': break 75 | n, = unpack('>Q', datum) 76 | values.append(n) 77 | 78 | start = time() 79 | 80 | sumT = cls() # sum of total prices 81 | sumB = cls() # sum of basic tax 82 | sumD = cls() # sum of 'distance' tax 83 | 84 | for n in values: 85 | calltype = n & 1 86 | r = rates[calltype] 87 | 88 | p = r * n 89 | b = p * basictax 90 | sumB += b 91 | t = p + b 92 | 93 | if calltype: 94 | d = p * disttax 95 | sumD += d 96 | t += d 97 | 98 | sumT += t 99 | 100 | end = time() 101 | return end - start 102 | 103 | 104 | def main(n, cls=Fraction): 105 | for _ in range(5): 106 | run(cls) # warmup 107 | times = [run(cls) for _ in range(n)] 108 | return times 109 | 110 | 111 | def percentile(values, percent): 112 | return values[len(values) * percent // 100] 113 | 114 | 115 | if __name__ == "__main__": 116 | import optparse 117 | parser = optparse.OptionParser( 118 | usage="%prog [options]", 119 | description="Test the performance of the Telco fractions benchmark") 120 | parser.add_option("-n", "--num_runs", action="store", type="int", default=200, 121 | dest="num_runs", help="Number of times to repeat the benchmark.") 122 | parser.add_option("--use-decimal", action="store_true", default=False, 123 | dest="use_decimal", help="Run benchmark with Decimal instead of Fraction.") 124 | parser.add_option("--use-stdlib", action="store_true", default=False, 125 | dest="use_stdlib", help="Run benchmark with fractions.Fraction from stdlib.") 126 | options, args = parser.parse_args() 127 | 128 | num_class = Fraction 129 | if options.use_decimal: 130 | from decimal import Decimal as num_class 131 | elif options.use_stdlib: 132 | from fractions import Fraction as num_class 133 | 134 | results = main(options.num_runs, num_class) 135 | #for result in results: 136 | # print(result) 137 | #print() 138 | 139 | results.sort() 140 | print('%.4f (15%%)' % percentile(results, 15)) 141 | print('%.4f (median)' % percentile(results, 50)) 142 | print('%.4f (85%%)' % percentile(results, 85)) 143 | print('%.4f (mean)' % (fsum(results[1:-1]) / (len(results) - 2))) 144 | -------------------------------------------------------------------------------- /.github/workflows/wheels.yml: -------------------------------------------------------------------------------- 1 | name: Wheel build 2 | 3 | on: 4 | release: 5 | types: [created] 6 | schedule: 7 | # ┌───────────── minute (0 - 59) 8 | # │ ┌───────────── hour (0 - 23) 9 | # │ │ ┌───────────── day of the month (1 - 31) 10 | # │ │ │ ┌───────────── month (1 - 12 or JAN-DEC) 11 | # │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT) 12 | # │ │ │ │ │ 13 | - cron: "42 3 * * 4" 14 | push: 15 | paths: 16 | - .github/workflows/wheels.yml 17 | - requirements.txt 18 | - pyproject.toml 19 | - MANIFEST.in 20 | - Makefile 21 | - setup.py 22 | pull_request: 23 | types: [opened, synchronize, reopened] 24 | paths: 25 | - .github/workflows/wheels.yml 26 | - requirements.txt 27 | - pyproject.toml 28 | - MANIFEST.in 29 | - Makefile 30 | - setup.py 31 | workflow_dispatch: 32 | 33 | concurrency: 34 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} 35 | cancel-in-progress: true 36 | 37 | permissions: {} 38 | 39 | jobs: 40 | sdist: 41 | runs-on: ubuntu-latest 42 | 43 | permissions: 44 | contents: write 45 | 46 | steps: 47 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4.1.1 48 | 49 | - name: Set up Python 50 | uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 51 | with: 52 | python-version: "3.x" 53 | 54 | - name: Install Python dependencies 55 | run: python -m pip install -U pip setuptools wheel && python -m pip install -U -r requirements.txt 56 | 57 | - name: Build sdist 58 | run: make sdist 59 | 60 | - name: Upload sdist 61 | uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 62 | with: 63 | name: sdist 64 | path: dist/*.tar.gz 65 | 66 | generate-wheels-matrix: 67 | # Create a matrix of all architectures & versions to build. 68 | # This enables the next step to run cibuildwheel in parallel. 69 | # From https://iscinumpy.dev/post/cibuildwheel-2-10-0/#only-210 70 | name: Generate wheels matrix 71 | runs-on: ubuntu-latest 72 | outputs: 73 | include: ${{ steps.set-matrix.outputs.include }} 74 | steps: 75 | - uses: actions/checkout@v6.0.1 76 | - name: Install cibuildwheel 77 | # Nb. keep cibuildwheel version pin consistent with job below 78 | run: pipx install cibuildwheel==3.3.0 79 | - id: set-matrix 80 | run: | 81 | MATRIX=$( 82 | { 83 | cibuildwheel --print-build-identifiers --platform linux \ 84 | | jq -nRc '{"only": inputs, "os": "ubuntu-latest"}' \ 85 | | sed -e '/aarch64\|armv7l/s|ubuntu-latest|ubuntu-24.04-arm|' \ 86 | && cibuildwheel --print-build-identifiers --platform macos \ 87 | | jq -nRc '{"only": inputs, "os": "macos-latest"}' \ 88 | && cibuildwheel --print-build-identifiers --platform windows \ 89 | | jq -nRc '{"only": inputs, "os": "windows-2022"}' \ 90 | && cibuildwheel --print-build-identifiers --platform windows --archs ARM64 \ 91 | | jq -nRc '{"only": inputs, "os": "windows-11-arm"}' 92 | } | jq -sc 93 | ) 94 | echo "include=$MATRIX" >> $GITHUB_OUTPUT 95 | 96 | build_wheels: 97 | name: Build ${{ matrix.only }} 98 | needs: generate-wheels-matrix 99 | runs-on: ${{ matrix.os }} 100 | 101 | strategy: 102 | fail-fast: false 103 | matrix: 104 | include: ${{ fromJson(needs.generate-wheels-matrix.outputs.include) }} 105 | 106 | steps: 107 | - name: Check out the repo 108 | uses: actions/checkout@v6.0.1 109 | 110 | - name: Set up QEMU 111 | if: runner.os == 'Linux' 112 | uses: docker/setup-qemu-action@v3 113 | with: 114 | platforms: all 115 | 116 | - name: Build wheels 117 | uses: pypa/cibuildwheel@v3.3.0 118 | with: 119 | only: ${{ matrix.only }} 120 | 121 | - name: Build faster Linux wheels 122 | # also build wheels with the most recent manylinux images and gcc 123 | if: runner.os == 'Linux' && !contains(matrix.only, 'i686') 124 | uses: pypa/cibuildwheel@v3.3.0 125 | env: 126 | CIBW_MANYLINUX_X86_64_IMAGE: manylinux_2_34 127 | CIBW_MANYLINUX_AARCH64_IMAGE: manylinux_2_34 # manylinux_2_39 ? 128 | CIBW_MANYLINUX_ARMV7L_IMAGE: manylinux_2_35 129 | CIBW_MANYLINUX_PPC64LE_IMAGE: manylinux_2_34 130 | CIBW_MANYLINUX_S390X_IMAGE: manylinux_2_34 131 | CIBW_MANYLINUX_RISCV64_IMAGE: manylinux_2_39 132 | CIBW_MANYLINUX_PYPY_X86_64_IMAGE: manylinux_2_34 133 | CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: manylinux_2_34 134 | CIBW_MUSLLINUX_X86_64_IMAGE: musllinux_1_2 135 | CIBW_MUSLLINUX_AARCH64_IMAGE: musllinux_1_2 136 | CIBW_MUSLLINUX_PPC64LE_IMAGE: musllinux_1_2 137 | CIBW_MUSLLINUX_S390X_IMAGE: musllinux_1_2 138 | with: 139 | only: ${{ matrix.only }} 140 | 141 | - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 142 | with: 143 | path: ./wheelhouse/*.whl 144 | name: wheels-${{ matrix.only }} 145 | 146 | merge_wheels: 147 | name: Merge wheel archives 148 | needs: build_wheels 149 | runs-on: ubuntu-latest 150 | 151 | steps: 152 | - name: Merge wheels 153 | uses: actions/upload-artifact/merge@v6 154 | with: 155 | name: all_wheels 156 | pattern: wheels-* 157 | delete-merged: true 158 | compression-level: 9 159 | 160 | upload_release_assets: 161 | name: Upload packages 162 | needs: [ sdist, merge_wheels ] 163 | runs-on: ubuntu-latest 164 | if: github.ref_type == 'tag' 165 | 166 | permissions: 167 | contents: write 168 | 169 | steps: 170 | - name: Check out the repo 171 | uses: actions/checkout@v6.0.1 172 | 173 | - name: Download files 174 | uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 175 | with: 176 | path: ./dist_downloads 177 | merge-multiple: true 178 | 179 | - name: List downloaded artifacts 180 | run: ls -la ./dist_downloads 181 | 182 | - name: Deduplicate wheels 183 | run: python3 dedup_wheels.py -d ./dist_downloads 184 | 185 | - name: Release 186 | uses: softprops/action-gh-release@v2 187 | with: 188 | files: ./dist_downloads/* 189 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI tests 2 | 3 | on: 4 | push: 5 | paths: 6 | - '**' 7 | - '!.github/**' 8 | - '.github/workflows/ci.yml' 9 | pull_request: 10 | paths: 11 | - '**' 12 | - '!.github/**' 13 | - '.github/workflows/ci.yml' 14 | 15 | concurrency: 16 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} 17 | cancel-in-progress: true 18 | 19 | permissions: 20 | contents: read # to fetch code (actions/checkout) 21 | 22 | 23 | jobs: 24 | sdist: 25 | runs-on: ubuntu-latest 26 | 27 | steps: 28 | - uses: actions/checkout@v6.0.1 29 | 30 | - name: Set up Python 31 | uses: actions/setup-python@v6.1.0 32 | with: 33 | python-version: "3.12" 34 | 35 | - name: Install Dependency 36 | run: | 37 | python -m pip install -U pip setuptools && python -m pip install -r requirements.txt 38 | 39 | - name: Build sdist 40 | run: make sdist 41 | 42 | - name: Archive sdist 43 | uses: actions/upload-artifact@v6 44 | with: 45 | name: sdist 46 | path: dist/*.tar.gz 47 | if-no-files-found: ignore 48 | 49 | Tests: 50 | strategy: 51 | # Allows for matrix sub-jobs to fail without canceling the rest 52 | fail-fast: false 53 | 54 | matrix: 55 | python-version: 56 | - "3.8" 57 | - "3.9" 58 | - "3.10" 59 | - "3.11" 60 | - "3.12" 61 | - "3.13" 62 | - "3.13t" 63 | - "3.14" 64 | - "3.14t" 65 | - "3.15-dev" 66 | - "3.15t-dev" 67 | os: ["ubuntu-latest"] 68 | 69 | #include: 70 | #- python-version: "3.7" 71 | # os: ubuntu-22.04 72 | #- python-version: "3.14-dev" 73 | # allowed_failure: true 74 | 75 | runs-on: ${{ matrix.os }} 76 | 77 | steps: 78 | - uses: actions/checkout@v6.0.1 79 | 80 | - name: Set up Python 81 | uses: actions/setup-python@v6.1.0 82 | with: 83 | python-version: ${{ matrix.python-version }} 84 | 85 | - name: Install Dependency 86 | run: | 87 | python -m pip install -U pip setuptools && python -m pip install -r requirements.txt 88 | 89 | - name: Build 90 | continue-on-error: ${{ matrix.allowed_failure || false }} 91 | run: make local 92 | 93 | - name: Run slow tests 94 | continue-on-error: ${{ matrix.allowed_failure || false }} 95 | run: make testslow 96 | 97 | - name: Running benchmark 98 | if: startsWith(matrix.python-version, '3.') 99 | run: | 100 | # Run different benchmarks. 101 | date; echo "Running telco benchmark ..." 102 | python benchmark/telco_fractions.py -n 250 103 | date; echo "Running micro benchmarks ..." 104 | python benchmark/microbench.py create pidigits 105 | date; echo "Done." 106 | 107 | Linux: 108 | 109 | strategy: 110 | # Allows for matrix sub-jobs to fail without canceling the rest 111 | fail-fast: false 112 | 113 | matrix: 114 | image: 115 | - manylinux_2_24_i686 116 | - manylinux_2_24_x86_64 117 | - manylinux_2_34_x86_64 118 | - musllinux_1_1_x86_64 119 | - musllinux_1_2_x86_64 120 | pyversion: ["cp"] 121 | 122 | include: 123 | - image: manylinux_2_24_aarch64 124 | pyversion: "cp38" 125 | - image: manylinux_2_24_aarch64 126 | pyversion: "cp39" 127 | - image: manylinux_2_24_aarch64 128 | pyversion: "cp310" 129 | - image: manylinux_2_24_aarch64 130 | pyversion: "cp311" 131 | - image: manylinux_2_34_aarch64 132 | pyversion: "cp312" 133 | - image: manylinux_2_34_aarch64 134 | pyversion: "cp313" 135 | - image: manylinux_2_34_aarch64 136 | pyversion: "cp314" 137 | 138 | runs-on: ubuntu-latest 139 | 140 | steps: 141 | - uses: actions/checkout@v6.0.1 142 | 143 | - name: Set up Python 144 | uses: actions/setup-python@v6.1.0 145 | with: 146 | python-version: "3.12" 147 | 148 | - name: Install Dependency 149 | run: | 150 | python -m pip install -U pip setuptools && python -m pip install --pre -r requirements.txt 151 | 152 | - name: Building wheel 153 | run: | 154 | make PYTHON_WHEEL_BUILD_VERSION="${{ matrix.pyversion }}*" sdist wheel_${{ matrix.image }} 155 | 156 | - name: Copy wheels in dist 157 | run: cp -v wheelhouse*/*-m*linux*.whl dist/ # manylinux / musllinux 158 | 159 | - name: Archive Wheels 160 | uses: actions/upload-artifact@v6 161 | with: 162 | name: wheels-${{ matrix.image }}-${{ matrix.pyversion }} 163 | path: dist/*.whl 164 | if-no-files-found: ignore 165 | 166 | non-Linux: 167 | strategy: 168 | # Allows for matrix sub-jobs to fail without canceling the rest 169 | fail-fast: false 170 | 171 | matrix: 172 | os: [macos-latest, windows-latest] 173 | python-version: 174 | - "3.8" 175 | - "3.9" 176 | - "3.10" 177 | - "3.11" 178 | - "3.12" 179 | - "3.13" 180 | - "3.13t" 181 | - "3.14" 182 | - "3.14t" 183 | - "3.15-dev" 184 | - "3.15t-dev" 185 | 186 | #include: 187 | # - python-version: "3.15-dev" 188 | # allowed_failure: true 189 | 190 | runs-on: ${{ matrix.os }} 191 | env: { MACOSX_DEPLOYMENT_TARGET: 11.0 } 192 | 193 | steps: 194 | - uses: actions/checkout@v6.0.1 195 | 196 | - name: Set up Python 197 | uses: actions/setup-python@v6.1.0 198 | with: 199 | python-version: ${{ matrix.python-version }} 200 | 201 | - name: Install dependencies 202 | run: | 203 | python -m pip install -U pip setuptools wheel 204 | python -m pip install --pre -r requirements.txt 205 | 206 | - name: Build wheels 207 | run: make sdist wheel 208 | 209 | - name: Run slow tests 210 | run: make testslow 211 | 212 | - name: Upload wheels 213 | uses: actions/upload-artifact@v6 214 | with: 215 | name: wheels-${{ matrix.os }}-${{ matrix.python-version }} 216 | path: dist/*.whl 217 | if-no-files-found: ignore 218 | 219 | - name: Running benchmark 220 | run: | 221 | # Run different benchmarks. 222 | date; echo "Running telco benchmark ..." 223 | python benchmark/telco_fractions.py -n 250 224 | date; echo "Running micro benchmarks ..." 225 | python benchmark/microbench.py create pidigits 226 | date; echo "Done." 227 | 228 | merge-wheels: 229 | needs: [ Linux, non-Linux ] 230 | runs-on: ubuntu-latest 231 | 232 | steps: 233 | - name: Merge wheels 234 | uses: actions/upload-artifact/merge@v6 235 | with: 236 | name: all_wheels 237 | pattern: wheels-* 238 | delete-merged: true 239 | compression-level: 9 240 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | ChangeLog 2 | ========= 3 | 4 | 1.23 (2025-??-??) 5 | ----------------- 6 | 7 | * Thousands separator in formatting was handled differently than for floats etc. 8 | https://github.com/python/cpython/pull/131067 9 | 10 | * Support thousands separators for formatting fractional part of Fraction. 11 | https://github.com/python/cpython/pull/132204 12 | 13 | 14 | 1.22 (2025-08-25) 15 | ----------------- 16 | 17 | * A choice of different GCD implementations is available via ``quicktions.use_gcd_impl()``. 18 | The fastest one on the current machine is chosen at import time. 19 | 20 | * Built using Cython 3.1.3. 21 | 22 | 23 | 1.21 (2025-06-13) 24 | ----------------- 25 | 26 | * A serious parser bug could accidentally concatenate numerator and denominator 27 | as final denominator when parsing "x/y" where x or y are close to ``sys.maxsize``, 28 | thus returning a ``Fraction("x/xy")``. 29 | 30 | * MSVC and clang now also benefit from fast "count trailing zeroes" intrinsics. 31 | 32 | 33 | 1.20 (2025-06-13) 34 | ----------------- 35 | 36 | * ``quicktions`` is compatible with freethreading Python (3.13+). 37 | 38 | * Accept leading zeros in precision/width for Fraction's formatting, following 39 | https://github.com/python/cpython/pull/130663 40 | 41 | * In line with Python's ``Fraction``, quicktions now raises a ``ValueError`` 42 | (instead of an ``OverflowError``) when exceeding parser limits, following 43 | https://github.com/python/cpython/pull/134010 44 | 45 | * Call ``__rpow__`` in ternary ``pow()`` if necessary, following 46 | https://github.com/python/cpython/pull/130251 47 | 48 | * Built using Cython 3.1.2. 49 | 50 | 51 | 1.19 (2024-11-29) 52 | ----------------- 53 | 54 | * Support for Python 2.7 as well as 3.7 and earlier has been removed. 55 | 56 | * Generally use ``.as_integer_ratio()`` in the constructor if available. 57 | https://github.com/python/cpython/pull/120271 58 | 59 | * Add a classmethod ``.from_number()`` that requires a number argument, not a string. 60 | https://github.com/python/cpython/pull/121800 61 | 62 | * Mixed calculations with other ``Rational`` classes could return the wrong type. 63 | https://github.com/python/cpython/issues/119189 64 | 65 | * In mixed calculations with ``complex``, the Fraction is now converted to ``float`` 66 | instead of ``complex`` to avoid certain corner cases in complex calculation. 67 | https://github.com/python/cpython/pull/119839 68 | 69 | * Using ``complex`` numbers in division shows better tracebacks. 70 | https://github.com/python/cpython/pull/102842 71 | 72 | * Subclass instantiations and calculations could fail in some cases. 73 | 74 | 75 | 1.18 (2024-04-03) 76 | ----------------- 77 | 78 | * New binary wheels were added built with gcc 12 (manylinux_2_28). 79 | 80 | * x86_64 wheels now require SSE4.2. 81 | 82 | * Built using Cython 3.0.10. 83 | 84 | 85 | 1.17 (2024-03-24) 86 | ----------------- 87 | 88 | * Math operations were sped up by inlined binary GCD calculation. 89 | 90 | 91 | 1.16 (2024-01-10) 92 | ----------------- 93 | 94 | * Formatting support was improved, following CPython 3.13a3 as of 95 | https://github.com/python/cpython/pull/111320 96 | 97 | * Add support for Python 3.13 by using Cython 3.0.8 and calling ``math.gcd()``. 98 | 99 | 100 | 1.15 (2023-08-27) 101 | ----------------- 102 | 103 | * Add support for Python 3.12 by using Cython 3.0.2. 104 | 105 | 106 | 1.14 (2023-03-19) 107 | ----------------- 108 | 109 | * Implement ``__format__`` for ``Fraction``, following 110 | https://github.com/python/cpython/pull/100161 111 | 112 | * Implement ``Fraction.is_integer()``, following 113 | https://github.com/python/cpython/issues/100488 114 | 115 | * ``Fraction.limit_denominator()`` is faster, following 116 | https://github.com/python/cpython/pull/93730 117 | 118 | * Internal creation of result Fractions is about 10% faster if the calculated 119 | numerator/denominator pair is already normalised, following 120 | https://github.com/python/cpython/pull/101780 121 | 122 | * Built using Cython 3.0.0b1. 123 | 124 | 125 | 1.13 (2022-01-11) 126 | ----------------- 127 | 128 | * Parsing very long numbers from a fraction string was very slow, even slower 129 | than ``fractions.Fraction``. The parser is now faster in all cases (and 130 | still much faster for shorter numbers). 131 | 132 | * ``Fraction`` did not implement ``__int__``. 133 | https://bugs.python.org/issue44547 134 | 135 | 136 | 1.12 (2022-01-07) 137 | ----------------- 138 | 139 | * Faster and more space friendly pickling and unpickling. 140 | https://bugs.python.org/issue44154 141 | 142 | * Algorithmically faster arithmetic for large denominators, although slower for 143 | small fraction components. 144 | https://bugs.python.org/issue43420 145 | Original patch for CPython by Sergey B. Kirpichev and Raymond Hettinger. 146 | 147 | * Make sure ``bool(Fraction)`` always returns a ``bool``. 148 | https://bugs.python.org/issue39274 149 | 150 | * Built using Cython 3.0.0a10. 151 | 152 | 153 | 1.11 (2019-12-19) 154 | ----------------- 155 | 156 | * Fix ``OverflowError`` when parsing string values with long decimal parts. 157 | 158 | 159 | 1.10 (2019-08-23) 160 | ----------------- 161 | 162 | * ``hash(fraction)`` is substantially faster in Py3.8+, following an optimisation 163 | in CPython 3.9 (https://bugs.python.org/issue37863). 164 | 165 | * New method ``fraction.as_integer_ratio()``. 166 | 167 | 168 | 1.9 (2018-12-26) 169 | ---------------- 170 | 171 | * Substantially faster normalisation (and therefore instantiation) in Py3.5+. 172 | 173 | * ``//`` (floordiv) now follows the expected rounding behaviour when used with 174 | floats (by converting to float first), and is much faster for integer operations. 175 | 176 | * Fix return type of divmod(), where the first item should be an integer. 177 | 178 | * Further speed up mod and divmod operations. 179 | 180 | 181 | 1.8 (2018-12-26) 182 | ---------------- 183 | 184 | * Faster mod and divmod calculation. 185 | 186 | 187 | 1.7 (2018-10-16) 188 | ---------------- 189 | 190 | * Faster normalisation and fraction string parsing. 191 | 192 | * Add support for Python 3.7. 193 | 194 | * Built using Cython 0.29. 195 | 196 | 197 | 1.6 (2018-03-23) 198 | ---------------- 199 | 200 | * Speed up Fraction creation from a string value by 3-5x. 201 | 202 | * Built using Cython 0.28.1. 203 | 204 | 205 | 1.5 (2017-10-22) 206 | ---------------- 207 | 208 | * Result of power operator (``**``) was not normalised for negative values. 209 | 210 | * Built using Cython 0.27.2. 211 | 212 | 213 | 1.4 (2017-09-16) 214 | ---------------- 215 | 216 | * Rebuilt using Cython 0.26.1 to improve support of Python 3.7. 217 | 218 | 219 | 1.3 (2016-07-24) 220 | ---------------- 221 | 222 | * repair the faster instantiation from Decimal values in Python 3.6 223 | 224 | * avoid potential glitch for certain large numbers in normalisation under Python 2.x 225 | 226 | 227 | 1.2 (2016-04-08) 228 | ---------------- 229 | 230 | * change hash function in Python 2.x to match that of ``fractions.Fraction`` 231 | 232 | 233 | 1.1 (2016-03-29) 234 | ---------------- 235 | 236 | * faster instantiation from float values 237 | 238 | * faster instantiation from Decimal values in Python 3.6 239 | 240 | 241 | 1.0 (2015-09-10) 242 | ---------------- 243 | 244 | * ``Fraction.imag`` property could return non-zero 245 | 246 | * parsing strings with long fraction parts could use an incorrect scale 247 | 248 | 249 | 0.7 (2014-10-09) 250 | ---------------- 251 | 252 | * faster instantiation from float and string values 253 | 254 | * fix test in Python 2.x 255 | 256 | 257 | 0.6 (2014-10-09) 258 | ---------------- 259 | 260 | * faster normalisation (and thus instantiation) 261 | 262 | 263 | 0.5 (2014-10-06) 264 | ---------------- 265 | 266 | * faster math operations 267 | 268 | 269 | 0.4 (2014-10-06) 270 | ---------------- 271 | 272 | * enable legacy division support in Python 2.x 273 | 274 | 275 | 0.3 (2014-10-05) 276 | ---------------- 277 | 278 | * minor behavioural fixes in corner cases under Python 2.x 279 | (now passes all test in Py2.7 as well) 280 | 281 | 282 | 0.2 (2014-10-03) 283 | ---------------- 284 | 285 | * cache hash value of Fractions 286 | 287 | 288 | 0.1 (2014-09-24) 289 | ---------------- 290 | 291 | * initial public release 292 | -------------------------------------------------------------------------------- /src/formatfloat_testcases.txt: -------------------------------------------------------------------------------- 1 | -- 'f' code formatting, with explicit precision (>= 0). Output always 2 | -- has the given number of places after the point; zeros are added if 3 | -- necessary to make this true. 4 | 5 | -- zeros 6 | %.0f 0 -> 0 7 | %.1f 0 -> 0.0 8 | %.2f 0 -> 0.00 9 | %.3f 0 -> 0.000 10 | %.50f 0 -> 0.00000000000000000000000000000000000000000000000000 11 | 12 | -- precision 0; result should never include a . 13 | %.0f 1.5 -> 2 14 | %.0f 2.5 -> 2 15 | %.0f 3.5 -> 4 16 | %.0f 0.0 -> 0 17 | %.0f 0.1 -> 0 18 | %.0f 0.001 -> 0 19 | %.0f 10.0 -> 10 20 | %.0f 10.1 -> 10 21 | %.0f 10.01 -> 10 22 | %.0f 123.456 -> 123 23 | %.0f 1234.56 -> 1235 24 | %.0f 1e49 -> 9999999999999999464902769475481793196872414789632 25 | %.0f 9.9999999999999987e+49 -> 99999999999999986860582406952576489172979654066176 26 | %.0f 1e50 -> 100000000000000007629769841091887003294964970946560 27 | 28 | -- precision 1 29 | %.1f 0.0001 -> 0.0 30 | %.1f 0.001 -> 0.0 31 | %.1f 0.01 -> 0.0 32 | %.1f 0.04 -> 0.0 33 | %.1f 0.06 -> 0.1 34 | %.1f 0.25 -> 0.2 35 | %.1f 0.75 -> 0.8 36 | %.1f 1.4 -> 1.4 37 | %.1f 1.5 -> 1.5 38 | %.1f 10.0 -> 10.0 39 | %.1f 1000.03 -> 1000.0 40 | %.1f 1234.5678 -> 1234.6 41 | %.1f 1234.7499 -> 1234.7 42 | %.1f 1234.75 -> 1234.8 43 | 44 | -- precision 2 45 | %.2f 0.0001 -> 0.00 46 | %.2f 0.001 -> 0.00 47 | %.2f 0.004999 -> 0.00 48 | %.2f 0.005001 -> 0.01 49 | %.2f 0.01 -> 0.01 50 | %.2f 0.125 -> 0.12 51 | %.2f 0.375 -> 0.38 52 | %.2f 1234500 -> 1234500.00 53 | %.2f 1234560 -> 1234560.00 54 | %.2f 1234567 -> 1234567.00 55 | %.2f 1234567.8 -> 1234567.80 56 | %.2f 1234567.89 -> 1234567.89 57 | %.2f 1234567.891 -> 1234567.89 58 | %.2f 1234567.8912 -> 1234567.89 59 | 60 | -- alternate form always includes a decimal point. This only 61 | -- makes a difference when the precision is 0. 62 | %#.0f 0 -> 0. 63 | %#.1f 0 -> 0.0 64 | %#.0f 1.5 -> 2. 65 | %#.0f 2.5 -> 2. 66 | %#.0f 10.1 -> 10. 67 | %#.0f 1234.56 -> 1235. 68 | %#.1f 1.4 -> 1.4 69 | %#.2f 0.375 -> 0.38 70 | 71 | -- if precision is omitted it defaults to 6 72 | %f 0 -> 0.000000 73 | %f 1230000 -> 1230000.000000 74 | %f 1234567 -> 1234567.000000 75 | %f 123.4567 -> 123.456700 76 | %f 1.23456789 -> 1.234568 77 | %f 0.00012 -> 0.000120 78 | %f 0.000123 -> 0.000123 79 | %f 0.00012345 -> 0.000123 80 | %f 0.000001 -> 0.000001 81 | %f 0.0000005001 -> 0.000001 82 | %f 0.0000004999 -> 0.000000 83 | 84 | -- 'e' code formatting with explicit precision (>= 0). Output should 85 | -- always have exactly the number of places after the point that were 86 | -- requested. 87 | 88 | -- zeros 89 | %.0e 0 -> 0e+00 90 | %.1e 0 -> 0.0e+00 91 | %.2e 0 -> 0.00e+00 92 | %.10e 0 -> 0.0000000000e+00 93 | %.50e 0 -> 0.00000000000000000000000000000000000000000000000000e+00 94 | 95 | -- precision 0. no decimal point in the output 96 | %.0e 0.01 -> 1e-02 97 | %.0e 0.1 -> 1e-01 98 | %.0e 1 -> 1e+00 99 | %.0e 10 -> 1e+01 100 | %.0e 100 -> 1e+02 101 | %.0e 0.012 -> 1e-02 102 | %.0e 0.12 -> 1e-01 103 | %.0e 1.2 -> 1e+00 104 | %.0e 12 -> 1e+01 105 | %.0e 120 -> 1e+02 106 | %.0e 123.456 -> 1e+02 107 | %.0e 0.000123456 -> 1e-04 108 | %.0e 123456000 -> 1e+08 109 | %.0e 0.5 -> 5e-01 110 | %.0e 1.4 -> 1e+00 111 | %.0e 1.5 -> 2e+00 112 | %.0e 1.6 -> 2e+00 113 | %.0e 2.4999999 -> 2e+00 114 | %.0e 2.5 -> 2e+00 115 | %.0e 2.5000001 -> 3e+00 116 | %.0e 3.499999999999 -> 3e+00 117 | %.0e 3.5 -> 4e+00 118 | %.0e 4.5 -> 4e+00 119 | %.0e 5.5 -> 6e+00 120 | %.0e 6.5 -> 6e+00 121 | %.0e 7.5 -> 8e+00 122 | %.0e 8.5 -> 8e+00 123 | %.0e 9.4999 -> 9e+00 124 | %.0e 9.5 -> 1e+01 125 | %.0e 10.5 -> 1e+01 126 | %.0e 14.999 -> 1e+01 127 | %.0e 15 -> 2e+01 128 | 129 | -- precision 1 130 | %.1e 0.0001 -> 1.0e-04 131 | %.1e 0.001 -> 1.0e-03 132 | %.1e 0.01 -> 1.0e-02 133 | %.1e 0.1 -> 1.0e-01 134 | %.1e 1 -> 1.0e+00 135 | %.1e 10 -> 1.0e+01 136 | %.1e 100 -> 1.0e+02 137 | %.1e 120 -> 1.2e+02 138 | %.1e 123 -> 1.2e+02 139 | %.1e 123.4 -> 1.2e+02 140 | 141 | -- precision 2 142 | %.2e 0.00013 -> 1.30e-04 143 | %.2e 0.000135 -> 1.35e-04 144 | %.2e 0.0001357 -> 1.36e-04 145 | %.2e 0.0001 -> 1.00e-04 146 | %.2e 0.001 -> 1.00e-03 147 | %.2e 0.01 -> 1.00e-02 148 | %.2e 0.1 -> 1.00e-01 149 | %.2e 1 -> 1.00e+00 150 | %.2e 10 -> 1.00e+01 151 | %.2e 100 -> 1.00e+02 152 | %.2e 1000 -> 1.00e+03 153 | %.2e 1500 -> 1.50e+03 154 | %.2e 1590 -> 1.59e+03 155 | %.2e 1598 -> 1.60e+03 156 | %.2e 1598.7 -> 1.60e+03 157 | %.2e 1598.76 -> 1.60e+03 158 | %.2e 9999 -> 1.00e+04 159 | 160 | -- omitted precision defaults to 6 161 | %e 0 -> 0.000000e+00 162 | %e 165 -> 1.650000e+02 163 | %e 1234567 -> 1.234567e+06 164 | %e 12345678 -> 1.234568e+07 165 | %e 1.1 -> 1.100000e+00 166 | 167 | -- alternate form always contains a decimal point. This only makes 168 | -- a difference when precision is 0. 169 | 170 | %#.0e 0.01 -> 1.e-02 171 | %#.0e 0.1 -> 1.e-01 172 | %#.0e 1 -> 1.e+00 173 | %#.0e 10 -> 1.e+01 174 | %#.0e 100 -> 1.e+02 175 | %#.0e 0.012 -> 1.e-02 176 | %#.0e 0.12 -> 1.e-01 177 | %#.0e 1.2 -> 1.e+00 178 | %#.0e 12 -> 1.e+01 179 | %#.0e 120 -> 1.e+02 180 | %#.0e 123.456 -> 1.e+02 181 | %#.0e 0.000123456 -> 1.e-04 182 | %#.0e 123456000 -> 1.e+08 183 | %#.0e 0.5 -> 5.e-01 184 | %#.0e 1.4 -> 1.e+00 185 | %#.0e 1.5 -> 2.e+00 186 | %#.0e 1.6 -> 2.e+00 187 | %#.0e 2.4999999 -> 2.e+00 188 | %#.0e 2.5 -> 2.e+00 189 | %#.0e 2.5000001 -> 3.e+00 190 | %#.0e 3.499999999999 -> 3.e+00 191 | %#.0e 3.5 -> 4.e+00 192 | %#.0e 4.5 -> 4.e+00 193 | %#.0e 5.5 -> 6.e+00 194 | %#.0e 6.5 -> 6.e+00 195 | %#.0e 7.5 -> 8.e+00 196 | %#.0e 8.5 -> 8.e+00 197 | %#.0e 9.4999 -> 9.e+00 198 | %#.0e 9.5 -> 1.e+01 199 | %#.0e 10.5 -> 1.e+01 200 | %#.0e 14.999 -> 1.e+01 201 | %#.0e 15 -> 2.e+01 202 | %#.1e 123.4 -> 1.2e+02 203 | %#.2e 0.0001357 -> 1.36e-04 204 | 205 | -- 'g' code formatting. 206 | 207 | -- zeros 208 | %.0g 0 -> 0 209 | %.1g 0 -> 0 210 | %.2g 0 -> 0 211 | %.3g 0 -> 0 212 | %.4g 0 -> 0 213 | %.10g 0 -> 0 214 | %.50g 0 -> 0 215 | %.100g 0 -> 0 216 | 217 | -- precision 0 doesn't make a lot of sense for the 'g' code (what does 218 | -- it mean to have no significant digits?); in practice, it's interpreted 219 | -- as identical to precision 1 220 | %.0g 1000 -> 1e+03 221 | %.0g 100 -> 1e+02 222 | %.0g 10 -> 1e+01 223 | %.0g 1 -> 1 224 | %.0g 0.1 -> 0.1 225 | %.0g 0.01 -> 0.01 226 | %.0g 1e-3 -> 0.001 227 | %.0g 1e-4 -> 0.0001 228 | %.0g 1e-5 -> 1e-05 229 | %.0g 1e-6 -> 1e-06 230 | %.0g 12 -> 1e+01 231 | %.0g 120 -> 1e+02 232 | %.0g 1.2 -> 1 233 | %.0g 0.12 -> 0.1 234 | %.0g 0.012 -> 0.01 235 | %.0g 0.0012 -> 0.001 236 | %.0g 0.00012 -> 0.0001 237 | %.0g 0.000012 -> 1e-05 238 | %.0g 0.0000012 -> 1e-06 239 | 240 | -- precision 1 identical to precision 0 241 | %.1g 1000 -> 1e+03 242 | %.1g 100 -> 1e+02 243 | %.1g 10 -> 1e+01 244 | %.1g 1 -> 1 245 | %.1g 0.1 -> 0.1 246 | %.1g 0.01 -> 0.01 247 | %.1g 1e-3 -> 0.001 248 | %.1g 1e-4 -> 0.0001 249 | %.1g 1e-5 -> 1e-05 250 | %.1g 1e-6 -> 1e-06 251 | %.1g 12 -> 1e+01 252 | %.1g 120 -> 1e+02 253 | %.1g 1.2 -> 1 254 | %.1g 0.12 -> 0.1 255 | %.1g 0.012 -> 0.01 256 | %.1g 0.0012 -> 0.001 257 | %.1g 0.00012 -> 0.0001 258 | %.1g 0.000012 -> 1e-05 259 | %.1g 0.0000012 -> 1e-06 260 | 261 | -- precision 2 262 | %.2g 1000 -> 1e+03 263 | %.2g 100 -> 1e+02 264 | %.2g 10 -> 10 265 | %.2g 1 -> 1 266 | %.2g 0.1 -> 0.1 267 | %.2g 0.01 -> 0.01 268 | %.2g 0.001 -> 0.001 269 | %.2g 1e-4 -> 0.0001 270 | %.2g 1e-5 -> 1e-05 271 | %.2g 1e-6 -> 1e-06 272 | %.2g 1234 -> 1.2e+03 273 | %.2g 123 -> 1.2e+02 274 | %.2g 12.3 -> 12 275 | %.2g 1.23 -> 1.2 276 | %.2g 0.123 -> 0.12 277 | %.2g 0.0123 -> 0.012 278 | %.2g 0.00123 -> 0.0012 279 | %.2g 0.000123 -> 0.00012 280 | %.2g 0.0000123 -> 1.2e-05 281 | 282 | -- bad cases from http://bugs.python.org/issue9980 283 | %.12g 38210.0 -> 38210 284 | %.12g 37210.0 -> 37210 285 | %.12g 36210.0 -> 36210 286 | 287 | -- alternate g formatting: always include decimal point and 288 | -- exactly significant digits. 289 | %#.0g 0 -> 0. 290 | %#.1g 0 -> 0. 291 | %#.2g 0 -> 0.0 292 | %#.3g 0 -> 0.00 293 | %#.4g 0 -> 0.000 294 | 295 | %#.0g 0.2 -> 0.2 296 | %#.1g 0.2 -> 0.2 297 | %#.2g 0.2 -> 0.20 298 | %#.3g 0.2 -> 0.200 299 | %#.4g 0.2 -> 0.2000 300 | %#.10g 0.2 -> 0.2000000000 301 | 302 | %#.0g 2 -> 2. 303 | %#.1g 2 -> 2. 304 | %#.2g 2 -> 2.0 305 | %#.3g 2 -> 2.00 306 | %#.4g 2 -> 2.000 307 | 308 | %#.0g 20 -> 2.e+01 309 | %#.1g 20 -> 2.e+01 310 | %#.2g 20 -> 20. 311 | %#.3g 20 -> 20.0 312 | %#.4g 20 -> 20.00 313 | 314 | %#.0g 234.56 -> 2.e+02 315 | %#.1g 234.56 -> 2.e+02 316 | %#.2g 234.56 -> 2.3e+02 317 | %#.3g 234.56 -> 235. 318 | %#.4g 234.56 -> 234.6 319 | %#.5g 234.56 -> 234.56 320 | %#.6g 234.56 -> 234.560 321 | 322 | -- repr formatting. Result always includes decimal point and at 323 | -- least one digit after the point, or an exponent. 324 | %r 0 -> 0.0 325 | %r 1 -> 1.0 326 | 327 | %r 0.01 -> 0.01 328 | %r 0.02 -> 0.02 329 | %r 0.03 -> 0.03 330 | %r 0.04 -> 0.04 331 | %r 0.05 -> 0.05 332 | 333 | -- values >= 1e16 get an exponent 334 | %r 10 -> 10.0 335 | %r 100 -> 100.0 336 | %r 1e15 -> 1000000000000000.0 337 | %r 9.999e15 -> 9999000000000000.0 338 | %r 9999999999999998 -> 9999999999999998.0 339 | %r 9999999999999999 -> 1e+16 340 | %r 1e16 -> 1e+16 341 | %r 1e17 -> 1e+17 342 | 343 | -- as do values < 1e-4 344 | %r 1e-3 -> 0.001 345 | %r 1.001e-4 -> 0.0001001 346 | %r 1.0000000000000001e-4 -> 0.0001 347 | %r 1.000000000000001e-4 -> 0.0001000000000000001 348 | %r 1.00000000001e-4 -> 0.000100000000001 349 | %r 1.0000000001e-4 -> 0.00010000000001 350 | %r 1e-4 -> 0.0001 351 | %r 0.99999999999999999e-4 -> 0.0001 352 | %r 0.9999999999999999e-4 -> 9.999999999999999e-05 353 | %r 0.999999999999e-4 -> 9.99999999999e-05 354 | %r 0.999e-4 -> 9.99e-05 355 | %r 1e-5 -> 1e-05 356 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | A. HISTORY OF THE SOFTWARE 2 | ========================== 3 | 4 | Python was created in the early 1990s by Guido van Rossum at Stichting 5 | Mathematisch Centrum (CWI, see http://www.cwi.nl) in the Netherlands 6 | as a successor of a language called ABC. Guido remains Python's 7 | principal author, although it includes many contributions from others. 8 | 9 | In 1995, Guido continued his work on Python at the Corporation for 10 | National Research Initiatives (CNRI, see http://www.cnri.reston.va.us) 11 | in Reston, Virginia where he released several versions of the 12 | software. 13 | 14 | In May 2000, Guido and the Python core development team moved to 15 | BeOpen.com to form the BeOpen PythonLabs team. In October of the same 16 | year, the PythonLabs team moved to Digital Creations (now Zope 17 | Corporation, see http://www.zope.com). In 2001, the Python Software 18 | Foundation (PSF, see http://www.python.org/psf/) was formed, a 19 | non-profit organization created specifically to own Python-related 20 | Intellectual Property. Zope Corporation is a sponsoring member of 21 | the PSF. 22 | 23 | All Python releases are Open Source (see http://www.opensource.org for 24 | the Open Source Definition). Historically, most, but not all, Python 25 | releases have also been GPL-compatible; the table below summarizes 26 | the various releases. 27 | 28 | Release Derived Year Owner GPL- 29 | from compatible? (1) 30 | 31 | 0.9.0 thru 1.2 1991-1995 CWI yes 32 | 1.3 thru 1.5.2 1.2 1995-1999 CNRI yes 33 | 1.6 1.5.2 2000 CNRI no 34 | 2.0 1.6 2000 BeOpen.com no 35 | 1.6.1 1.6 2001 CNRI yes (2) 36 | 2.1 2.0+1.6.1 2001 PSF no 37 | 2.0.1 2.0+1.6.1 2001 PSF yes 38 | 2.1.1 2.1+2.0.1 2001 PSF yes 39 | 2.1.2 2.1.1 2002 PSF yes 40 | 2.1.3 2.1.2 2002 PSF yes 41 | 2.2 and above 2.1.1 2001-now PSF yes 42 | 43 | Footnotes: 44 | 45 | (1) GPL-compatible doesn't mean that we're distributing Python under 46 | the GPL. All Python licenses, unlike the GPL, let you distribute 47 | a modified version without making your changes open source. The 48 | GPL-compatible licenses make it possible to combine Python with 49 | other software that is released under the GPL; the others don't. 50 | 51 | (2) According to Richard Stallman, 1.6.1 is not GPL-compatible, 52 | because its license has a choice of law clause. According to 53 | CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1 54 | is "not incompatible" with the GPL. 55 | 56 | Thanks to the many outside volunteers who have worked under Guido's 57 | direction to make these releases possible. 58 | 59 | 60 | B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON 61 | =============================================================== 62 | 63 | PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 64 | -------------------------------------------- 65 | 66 | 1. This LICENSE AGREEMENT is between the Python Software Foundation 67 | ("PSF"), and the Individual or Organization ("Licensee") accessing and 68 | otherwise using this software ("Python") in source or binary form and 69 | its associated documentation. 70 | 71 | 2. Subject to the terms and conditions of this License Agreement, PSF hereby 72 | grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, 73 | analyze, test, perform and/or display publicly, prepare derivative works, 74 | distribute, and otherwise use Python alone or in any derivative version, 75 | provided, however, that PSF's License Agreement and PSF's notice of copyright, 76 | i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 77 | 2011, 2012, 2013, 2014 Python Software Foundation; All Rights Reserved" are 78 | retained in Python alone or in any derivative version prepared by Licensee. 79 | 80 | 3. In the event Licensee prepares a derivative work that is based on 81 | or incorporates Python or any part thereof, and wants to make 82 | the derivative work available to others as provided herein, then 83 | Licensee hereby agrees to include in any such work a brief summary of 84 | the changes made to Python. 85 | 86 | 4. PSF is making Python available to Licensee on an "AS IS" 87 | basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR 88 | IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND 89 | DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS 90 | FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT 91 | INFRINGE ANY THIRD PARTY RIGHTS. 92 | 93 | 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON 94 | FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS 95 | A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, 96 | OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. 97 | 98 | 6. This License Agreement will automatically terminate upon a material 99 | breach of its terms and conditions. 100 | 101 | 7. Nothing in this License Agreement shall be deemed to create any 102 | relationship of agency, partnership, or joint venture between PSF and 103 | Licensee. This License Agreement does not grant permission to use PSF 104 | trademarks or trade name in a trademark sense to endorse or promote 105 | products or services of Licensee, or any third party. 106 | 107 | 8. By copying, installing or otherwise using Python, Licensee 108 | agrees to be bound by the terms and conditions of this License 109 | Agreement. 110 | 111 | 112 | BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0 113 | ------------------------------------------- 114 | 115 | BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1 116 | 117 | 1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an 118 | office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the 119 | Individual or Organization ("Licensee") accessing and otherwise using 120 | this software in source or binary form and its associated 121 | documentation ("the Software"). 122 | 123 | 2. Subject to the terms and conditions of this BeOpen Python License 124 | Agreement, BeOpen hereby grants Licensee a non-exclusive, 125 | royalty-free, world-wide license to reproduce, analyze, test, perform 126 | and/or display publicly, prepare derivative works, distribute, and 127 | otherwise use the Software alone or in any derivative version, 128 | provided, however, that the BeOpen Python License is retained in the 129 | Software, alone or in any derivative version prepared by Licensee. 130 | 131 | 3. BeOpen is making the Software available to Licensee on an "AS IS" 132 | basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR 133 | IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND 134 | DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS 135 | FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT 136 | INFRINGE ANY THIRD PARTY RIGHTS. 137 | 138 | 4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE 139 | SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS 140 | AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY 141 | DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. 142 | 143 | 5. This License Agreement will automatically terminate upon a material 144 | breach of its terms and conditions. 145 | 146 | 6. This License Agreement shall be governed by and interpreted in all 147 | respects by the law of the State of California, excluding conflict of 148 | law provisions. Nothing in this License Agreement shall be deemed to 149 | create any relationship of agency, partnership, or joint venture 150 | between BeOpen and Licensee. This License Agreement does not grant 151 | permission to use BeOpen trademarks or trade names in a trademark 152 | sense to endorse or promote products or services of Licensee, or any 153 | third party. As an exception, the "BeOpen Python" logos available at 154 | http://www.pythonlabs.com/logos.html may be used according to the 155 | permissions granted on that web page. 156 | 157 | 7. By copying, installing or otherwise using the software, Licensee 158 | agrees to be bound by the terms and conditions of this License 159 | Agreement. 160 | 161 | 162 | CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1 163 | --------------------------------------- 164 | 165 | 1. This LICENSE AGREEMENT is between the Corporation for National 166 | Research Initiatives, having an office at 1895 Preston White Drive, 167 | Reston, VA 20191 ("CNRI"), and the Individual or Organization 168 | ("Licensee") accessing and otherwise using Python 1.6.1 software in 169 | source or binary form and its associated documentation. 170 | 171 | 2. Subject to the terms and conditions of this License Agreement, CNRI 172 | hereby grants Licensee a nonexclusive, royalty-free, world-wide 173 | license to reproduce, analyze, test, perform and/or display publicly, 174 | prepare derivative works, distribute, and otherwise use Python 1.6.1 175 | alone or in any derivative version, provided, however, that CNRI's 176 | License Agreement and CNRI's notice of copyright, i.e., "Copyright (c) 177 | 1995-2001 Corporation for National Research Initiatives; All Rights 178 | Reserved" are retained in Python 1.6.1 alone or in any derivative 179 | version prepared by Licensee. Alternately, in lieu of CNRI's License 180 | Agreement, Licensee may substitute the following text (omitting the 181 | quotes): "Python 1.6.1 is made available subject to the terms and 182 | conditions in CNRI's License Agreement. This Agreement together with 183 | Python 1.6.1 may be located on the Internet using the following 184 | unique, persistent identifier (known as a handle): 1895.22/1013. This 185 | Agreement may also be obtained from a proxy server on the Internet 186 | using the following URL: http://hdl.handle.net/1895.22/1013". 187 | 188 | 3. In the event Licensee prepares a derivative work that is based on 189 | or incorporates Python 1.6.1 or any part thereof, and wants to make 190 | the derivative work available to others as provided herein, then 191 | Licensee hereby agrees to include in any such work a brief summary of 192 | the changes made to Python 1.6.1. 193 | 194 | 4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS" 195 | basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR 196 | IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND 197 | DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS 198 | FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT 199 | INFRINGE ANY THIRD PARTY RIGHTS. 200 | 201 | 5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON 202 | 1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS 203 | A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1, 204 | OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. 205 | 206 | 6. This License Agreement will automatically terminate upon a material 207 | breach of its terms and conditions. 208 | 209 | 7. This License Agreement shall be governed by the federal 210 | intellectual property law of the United States, including without 211 | limitation the federal copyright law, and, to the extent such 212 | U.S. federal law does not apply, by the law of the Commonwealth of 213 | Virginia, excluding Virginia's conflict of law provisions. 214 | Notwithstanding the foregoing, with regard to derivative works based 215 | on Python 1.6.1 that incorporate non-separable material that was 216 | previously distributed under the GNU General Public License (GPL), the 217 | law of the Commonwealth of Virginia shall govern this License 218 | Agreement only as to issues arising under or with respect to 219 | Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this 220 | License Agreement shall be deemed to create any relationship of 221 | agency, partnership, or joint venture between CNRI and Licensee. This 222 | License Agreement does not grant permission to use CNRI trademarks or 223 | trade name in a trademark sense to endorse or promote products or 224 | services of Licensee, or any third party. 225 | 226 | 8. By clicking on the "ACCEPT" button where indicated, or by copying, 227 | installing or otherwise using Python 1.6.1, Licensee agrees to be 228 | bound by the terms and conditions of this License Agreement. 229 | 230 | ACCEPT 231 | 232 | 233 | CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2 234 | -------------------------------------------------- 235 | 236 | Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam, 237 | The Netherlands. All rights reserved. 238 | 239 | Permission to use, copy, modify, and distribute this software and its 240 | documentation for any purpose and without fee is hereby granted, 241 | provided that the above copyright notice appear in all copies and that 242 | both that copyright notice and this permission notice appear in 243 | supporting documentation, and that the name of Stichting Mathematisch 244 | Centrum or CWI not be used in advertising or publicity pertaining to 245 | distribution of the software without specific, written prior 246 | permission. 247 | 248 | STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO 249 | THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 250 | FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE 251 | FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 252 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 253 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT 254 | OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 255 | -------------------------------------------------------------------------------- /benchmark/microbench.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import itertools 3 | import operator 4 | import os 5 | import statistics 6 | import timeit 7 | from collections import defaultdict 8 | 9 | from decimal import Decimal 10 | from fractions import Fraction as PyFraction 11 | 12 | def rel_path(*path): 13 | return os.path.join(os.path.dirname(__file__), *path) 14 | 15 | try: 16 | from quicktions import Fraction as QFraction 17 | except ImportError: 18 | import sys 19 | sys.path.insert(0, rel_path('..', 'src')) 20 | from quicktions import Fraction as QFraction 21 | 22 | PyFraction.__name__ = "PyFraction" 23 | 24 | class_names, classes = list(zip(*[ 25 | ('float', float), 26 | ('Decimal', Decimal), 27 | ('QFraction', QFraction), 28 | ('PyFraction', PyFraction), 29 | ])) 30 | 31 | benchmark_values = [ 32 | "123", 33 | "76138945735803", 34 | "123.456789", 35 | "1234e10", 36 | "1234e-10", 37 | "983675298736458927343e10", 38 | "983675298736458927343e-10", 39 | "983675298736458927345e10", 40 | "983675298736458927345e-10", 41 | "983675298736458927343e50", 42 | "983675298736458927343e-50", 43 | "983675298736458927345e60", 44 | "983675298736458927345e-60", 45 | ] 46 | 47 | 48 | benchmark_expressions = [ 49 | "a + b + c", 50 | "a - b - c", 51 | "a * b * c", 52 | "a / b / c", 53 | "a * 10 + b * 99 + c", 54 | "a * 10 * b + c", 55 | "a / 10 / b - c", 56 | "a * 32543575324 * b / c", 57 | "(a * 32543575324) / (b * 65767564) * c", 58 | "a ** 3 + b ** 2 + c ** 1", 59 | ] 60 | 61 | PI_DIGITS = ( 62 | "3.141592653589793238462643383279502884197169399375105820974944592307816406286" 63 | "208998628034825342117067982148086513282306647093844609550582231725359408128481" 64 | "117450284102701938521105559644622948954930381964428810975665933446128475648233" 65 | "786783165271201909145648566923460348610454326648213393607260249141273724587006" 66 | "606315588174881520920962829254091715364367892590360011330530548820466521384146" 67 | "951941511609433057270365759591953092186117381932611793105118548074462379962749" 68 | "567351885752724891227938183011949129833673362440656643086021394946395224737190" 69 | "702179860943702770539217176293176752384674818467669405132000568127145263560827" 70 | "785771342757789609173637178721468440901224953430146549585371050792279689258923" 71 | "542019956112129021960864034418159813629774771309960518707211349999998372978049" 72 | "951059731732816096318595024459455346908302642522308253344685035261931188171010" 73 | "003137838752886587533208381420617177669147303598253490428755468731159562863882" 74 | "353787593751957781857780532171226806613001927876611195909216420198938095257201" 75 | "065485863278865936153381827968230301952035301852968995773622599413891249721775" 76 | "283479131515574857242454150695950829533116861727855889075098381754637464939319" 77 | "255060400927701671139009848824012858361603563707660104710181942955596198946767" 78 | "837449448255379774726847104047534646208046684259069491293313677028989152104752" 79 | "162056966024058038150193511253382430035587640247496473263914199272604269922796" 80 | "782354781636009341721641219924586315030286182974555706749838505494588586926995" 81 | "690927210797509302955321165344987202755960236480665499119881834797753566369807" 82 | "426542527862551818417574672890977772793800081647060016145249192173217214772350" 83 | "141441973568548161361157352552133475741849468438523323907394143334547762416862" 84 | "518983569485562099219222184272550254256887671790494601653466804988627232791786" 85 | "085784383827967976681454100953883786360950680064225125205117392984896084128488" 86 | "626945604241965285022210661186306744278622039194945047123713786960956364371917" 87 | "287467764657573962413890865832645995813390478027590099465764078951269468398352" 88 | "595709825822620522489407726719478268482601476990902640136394437455305068203496" 89 | "252451749399651431429809190659250937221696461515709858387410597885959772975498" 90 | "930161753928468138268683868942774155991855925245953959431049972524680845987273" 91 | "644695848653836736222626099124608051243884390451244136549762780797715691435997" 92 | "700129616089441694868555848406353422072225828488648158456028506016842739452267" 93 | "467678895252138522549954666727823986456596116354886230577456498035593634568174" 94 | "324112515076069479451096596094025228879710893145669136867228748940560101503308" 95 | "617928680920874760917824938589009714909675985261365549781893129784821682998948" 96 | "722658804857564014270477555132379641451523746234364542858444795265867821051141" 97 | "354735739523113427166102135969536231442952484937187110145765403590279934403742" 98 | "007310578539062198387447808478489683321445713868751943506430218453191048481005" 99 | "370614680674919278191197939952061419663428754440643745123718192179998391015919" 100 | "561814675142691239748940907186494231961567945208095146550225231603881930142093" 101 | "762137855956638937787083039069792077346722182562599661501421503068038447734549" 102 | "202605414665925201497442850732518666002132434088190710486331734649651453905796" 103 | "268561005508106658796998163574736384052571459102897064140110971206280439039759" 104 | "515677157700420337869936007230558763176359421873125147120532928191826186125867" 105 | "321579198414848829164470609575270695722091756711672291098169091528017350671274" 106 | "858322287183520935396572512108357915136988209144421006751033467110314126711136" 107 | "990865851639831501970165151168517143765761835155650884909989859982387345528331" 108 | "635507647918535893226185489632132933089857064204675259070915481416549859461637" 109 | "180270981994309924488957571282890592323326097299712084433573265489382391193259" 110 | "746366730583604142813883032038249037589852437441702913276561809377344403070746" 111 | "921120191302033038019762110110044929321516084244485963766983895228684783123552" 112 | "658213144957685726243344189303968642624341077322697802807318915441101044682325" 113 | "271620105265227211166039666557309254711055785376346682065310989652691862056476" 114 | "931257058635662018558100729360659876486117910453348850346113657686753249441668" 115 | "039626579787718556084552965412665408530614344431858676975145661406800700237877" 116 | "659134401712749470420562230538994561314071127000407854733269939081454664645880" 117 | "797270826683063432858785698305235808933065757406795457163775254202114955761581" 118 | "400250126228594130216471550979259230990796547376125517656751357517829666454779" 119 | "174501129961489030463994713296210734043751895735961458901938971311179042978285" 120 | "647503203198691514028708085990480109412147221317947647772622414254854540332157" 121 | "185306142288137585043063321751829798662237172159160771669254748738986654949450" 122 | "114654062843366393790039769265672146385306736096571209180763832716641627488880" 123 | "078692560290228472104031721186082041900042296617119637792133757511495950156604" 124 | "963186294726547364252308177036751590673502350728354056704038674351362222477158" 125 | "915049530984448933309634087807693259939780541934144737744184263129860809988868" 126 | "741326047215695162396586457302163159819319516735381297416772947867242292465436" 127 | "680098067692823828068996400482435403701416314965897940924323789690706977942236" 128 | "250822168895738379862300159377647165122893578601588161755782973523344604281512" 129 | "627203734314653197777416031990665541876397929334419521541341899485444734567383" 130 | "162499341913181480927777103863877343177207545654532207770921201905166096280490" 131 | "926360197598828161332316663652861932668633606273567630354477628035045077723554" 132 | "710585954870279081435624014517180624643626794561275318134078330336254232783944" 133 | "975382437205835311477119926063813346776879695970309833913077109870408591337464" 134 | "144282277263465947047458784778720192771528073176790770715721344473060570073349" 135 | "243693113835049316312840425121925651798069411352801314701304781643788518529092" 136 | "854520116583934196562134914341595625865865570552690496520985803385072242648293" 137 | "972858478316305777756068887644624824685792603953527734803048029005876075825104" 138 | "747091643961362676044925627420420832085661190625454337213153595845068772460290" 139 | "161876679524061634252257719542916299193064553779914037340432875262888963995879" 140 | "475729174642635745525407909145135711136941091193932519107602082520261879853188" 141 | "770584297259167781314969900901921169717372784768472686084900337702424291651300" 142 | "500516832336435038951702989392233451722013812806965011784408745196012122859937" 143 | "162313017114448464090389064495444006198690754851602632750529834918740786680881" 144 | "833851022833450850486082503930213321971551843063545500766828294930413776552793" 145 | "975175461395398468339363830474611996653858153842056853386218672523340283087112" 146 | "328278921250771262946322956398989893582116745627010218356462201349671518819097" 147 | "303811980049734072396103685406643193950979019069963955245300545058068550195673" 148 | "022921913933918568034490398205955100226353536192041994745538593810234395544959" 149 | "778377902374216172711172364343543947822181852862408514006660443325888569867054" 150 | "315470696574745855033232334210730154594051655379068662733379958511562578432298" 151 | "827372319898757141595781119635833005940873068121602876496286744604774649159950" 152 | "549737425626901049037781986835938146574126804925648798556145372347867330390468" 153 | "838343634655379498641927056387293174872332083760112302991136793862708943879936" 154 | "201629515413371424892830722012690147546684765357616477379467520049075715552781" 155 | "965362132392640616013635815590742202020318727760527721900556148425551879253034" 156 | "351398442532234157623361064250639049750086562710953591946589751413103482276930" 157 | "624743536325691607815478181152843667957061108615331504452127473924544945423682" 158 | "886061340841486377670096120715124914043027253860764823634143346235189757664521" 159 | "641376796903149501910857598442391986291642193994907236234646844117394032659184" 160 | "044378051333894525742399508296591228508555821572503107125701266830240292952522" 161 | "011872676756220415420516184163484756516999811614101002996078386909291603028840" 162 | "026910414079288621507842451670908700069928212066041837180653556725253256753286" 163 | "129104248776182582976515795984703562226293486003415872298053498965022629174878" 164 | "820273420922224533985626476691490556284250391275771028402799806636582548892648" 165 | "802545661017296702664076559042909945681506526530537182941270336931378517860904" 166 | "070866711496558343434769338578171138645587367812301458768712660348913909562009" 167 | "939361031029161615288138437909904231747336394804575931493140529763475748119356" 168 | "709110137751721008031559024853090669203767192203322909433467685142214477379393" 169 | "751703443661991040337511173547191855046449026365512816228824462575916333039107" 170 | "225383742182140883508657391771509682887478265699599574490661758344137522397096" 171 | "834080053559849175417381883999446974867626551658276584835884531427756879002909" 172 | "517028352971634456212964043523117600665101241200659755851276178583829204197484" 173 | "423608007193045761893234922927965019875187212726750798125547095890455635792122" 174 | "103334669749923563025494780249011419521238281530911407907386025152274299581807" 175 | "247162591668545133312394804947079119153267343028244186041426363954800044800267" 176 | "049624820179289647669758318327131425170296923488962766844032326092752496035799" 177 | "646925650493681836090032380929345958897069536534940603402166544375589004563288" 178 | "225054525564056448246515187547119621844396582533754388569094113031509526179378" 179 | "002974120766514793942590298969594699556576121865619673378623625612521632086286" 180 | "922210327488921865436480229678070576561514463204692790682120738837781423356282" 181 | "360896320806822246801224826117718589638140918390367367222088832151375560037279" 182 | "839400415297002878307667094447456013455641725437090697939612257142989467154357" 183 | "846878861444581231459357198492252847160504922124247014121478057345510500801908" 184 | "699603302763478708108175450119307141223390866393833952942578690507643100638351" 185 | "983438934159613185434754649556978103829309716465143840700707360411237359984345" 186 | "225161050702705623526601276484830840761183013052793205427462865403603674532865" 187 | "105706587488225698157936789766974220575059683440869735020141020672358502007245" 188 | "225632651341055924019027421624843914035998953539459094407046912091409387001264" 189 | "560016237428802109276457931065792295524988727584610126483699989225695968815920" 190 | "560010165525637567856672279661988578279484885583439751874454551296563443480396" 191 | "642055798293680435220277098429423253302257634180703947699415979159453006975214" 192 | "829336655566156787364005366656416547321704390352132954352916941459904160875320" 193 | "186837937023488868947915107163785290234529244077365949563051007421087142613497" 194 | "459561513849871375704710178795731042296906667021449863746459528082436944578977" 195 | "233004876476524133907592043401963403911473202338071509522201068256342747164602" 196 | "433544005152126693249341967397704159568375355516673027390074972973635496453328" 197 | "886984406119649616277344951827369558822075735517665158985519098666539354948106" 198 | "887320685990754079234240230092590070173196036225475647894064754834664776041146" 199 | "323390565134330684495397907090302346046147096169688688501408347040546074295869" 200 | "913829668246818571031887906528703665083243197440477185567893482308943106828702" 201 | "722809736248093996270607472645539925399442808113736943388729406307926159599546" 202 | "262462970706259484556903471197299640908941805953439325123623550813494900436427" 203 | "852713831591256898929519642728757394691427253436694153236100453730488198551706" 204 | "594121735246258954873016760029886592578662856124966552353382942878542534048308" 205 | "330701653722856355915253478445981831341129001999205981352205117336585640782648" 206 | "494276441137639386692480311836445369858917544264739988228462184490087776977631" 207 | "279572267265556259628254276531830013407092233436577916012809317940171859859993" 208 | "384923549564005709955856113498025249906698423301735035804408116855265311709957" 209 | "089942732870925848789443646005041089226691783525870785951298344172953519537885" 210 | "534573742608590290817651557803905946408735061232261120093731080485485263572282" 211 | "576820341605048466277504500312620080079980492548534694146977516493270950493463" 212 | "938243222718851597405470214828971117779237612257887347718819682546298126868581" 213 | "705074027255026332904497627789442362167411918626943965067151577958675648239939" 214 | "176042601763387045499017614364120469218237076488783419689686118155815873606293" 215 | "860381017121585527266830082383404656475880405138080163363887421637140643549556" 216 | "186896411228214075330265510042410489678352858829024367090488711819090949453314" 217 | "421828766181031007354770549815968077200947469613436092861484941785017180779306" 218 | "810854690009445899527942439813921350558642219648349151263901280383200109773868" 219 | "066287792397180146134324457264009737425700735921003154150893679300816998053652" 220 | "027600727749674584002836240534603726341655425902760183484030681138185510597970" 221 | "566400750942608788573579603732451414678670368809880609716425849759513806930944" 222 | "940151542222194329130217391253835591503100333032511174915696917450271494331515" 223 | "588540392216409722910112903552181576282328318234254832611191280092825256190205" 224 | "263016391147724733148573910777587442538761174657867116941477642144111126358355" 225 | "387136101102326798775641024682403226483464176636980663785768134920453022408197" 226 | "278564719839630878154322116691224641591177673225326433568614618654522268126887" 227 | "268445968442416107854016768142080885028005414361314623082102594173756238994207" 228 | "571362751674573189189456283525704413354375857534269869947254703165661399199968" 229 | "262824727064133622217892390317608542894373393561889165125042440400895271983787" 230 | "386480584726895462438823437517885201439560057104811949884239060613695734231559" 231 | "079670346149143447886360410318235073650277859089757827273130504889398900992391" 232 | "350337325085598265586708924261242947367019390772713070686917092646254842324074" 233 | "855036608013604668951184009366860954632500214585293095000090715105823626729326" 234 | "453738210493872499669933942468551648326113414611068026744663733437534076429402" 235 | "668297386522093570162638464852851490362932019919968828517183953669134522244470" 236 | "804592396602817156551565666111359823112250628905854914509715755390024393153519" 237 | "090210711945730024388017661503527086260253788179751947806101371500448991721002" 238 | "220133501310601639154158957803711779277522597874289191791552241718958536168059" 239 | "474123419339842021874564925644346239253195313510331147639491199507285843065836" 240 | "193536932969928983791494193940608572486396883690326556436421664425760791471086" 241 | "998431573374964883529276932822076294728238153740996154559879825989109371712621" 242 | "828302584811238901196822142945766758071865380650648702613389282299497257453033" 243 | "283896381843944770779402284359883410035838" 244 | ) 245 | 246 | 247 | def all_types(values, for_cls): 248 | # "123.456" => "'123.456'" -- "123.456" -- "123456, 1000" 249 | if for_cls in (PyFraction, QFraction): 250 | def int_args(a, b): 251 | return f"{a}, {b}" 252 | def types(v): 253 | f = QFraction(v).as_integer_ratio() 254 | return ( 255 | repr(v), 256 | v, 257 | '"%d/%d"' % f, 258 | int_args(f[0], f[1]), 259 | int_args(f[0]*10, f[1]*100), 260 | int_args(f[0]*99, f[1]*f[0] - 1), 261 | int_args(f[0]*64*64, f[1]*1024*1024), 262 | ) 263 | else: 264 | def types(v): 265 | return ( 266 | repr(v), 267 | v, 268 | ) 269 | 270 | return [ 271 | tp_v 272 | for v in values 273 | for tp_v in types(v) 274 | ] 275 | 276 | 277 | def run_bm(code, setup='pass', number=200, repeat=1_000, gns=None): 278 | times = timeit.repeat(code, setup, number=number, repeat=repeat, globals=gns) 279 | p10 = len(times) // 10 280 | if p10 >= 1: 281 | times.sort() 282 | times = times[p10:-p10] 283 | return statistics.mean(times) * 1_000_000 # s -> us 284 | 285 | 286 | def bm_pidigits(pi_string=PI_DIGITS, classes=classes): 287 | gns = {} 288 | for cls in classes: 289 | cls_name = cls.__name__ 290 | gns[cls_name] = cls 291 | gns['s'] = pi_string 292 | results = {} 293 | code = f"all({cls_name}(s[:i]) for i in range(3, {len(pi_string)}, 265))" 294 | results[code] = run_bm(code, number=30, repeat=1, gns=gns) 295 | yield (cls, results) 296 | 297 | 298 | def bm_create(values=benchmark_values, classes=classes): 299 | gns = {} 300 | for cls in classes: 301 | cls_name = cls.__name__ 302 | gns[cls_name] = cls 303 | results = {} 304 | for args in all_types(values, cls): 305 | code = f"{cls_name}({args})" 306 | results[code] = run_bm(code, gns=gns) 307 | yield (cls, results) 308 | 309 | 310 | def bm_calculation(expressions=benchmark_expressions, values=benchmark_values, classes=classes): 311 | gns = {} 312 | for cls in classes: 313 | results = defaultdict(list) 314 | cls_name = cls.__name__ 315 | for a, b, c in itertools.product(values, repeat=3): 316 | gns.update(a=cls(a), b=cls(b), c=cls(c)) 317 | 318 | for expr in expressions: 319 | results[f"{cls_name}: {expr}"].append(run_bm(expr, number=30, repeat=100, gns=gns)) 320 | 321 | # average over all value combinations 322 | results = {expr: statistics.mean(t) for expr, t in results.items()} 323 | yield (cls, results) 324 | 325 | 326 | def run_benchmarks(benchmarks=()): 327 | for name, bm in [ 328 | ('create', bm_create), 329 | ('compute', bm_calculation), 330 | ('pidigits', bm_pidigits), 331 | ]: 332 | if benchmarks and name not in benchmarks: 333 | continue 334 | for cls, result in bm(): 335 | yield ((name, cls), result) 336 | 337 | 338 | def main(*benchmarks): 339 | results_by_type = defaultdict(dict) 340 | now = datetime.datetime.now 341 | start_time = now() 342 | print(start_time) 343 | for key, res in run_benchmarks(benchmarks): 344 | for code, t in sorted(res.items()): 345 | print(f"{code:50s}: {t:8.2f} us") 346 | results_by_type[key].update(res) 347 | print("++", datetime.timedelta(seconds=(now() - start_time).seconds)) 348 | 349 | avg_by_type = defaultdict(dict) 350 | for (what, cls), results in results_by_type.items(): 351 | avg_by_type[what][cls] = statistics.mean(results.values()) 352 | 353 | for what, timings in sorted(avg_by_type.items(), reverse=True): 354 | print() 355 | print(f"Average times for all '{what}' benchmarks:") 356 | min_avg = min(timings.values()) 357 | for cls, avg in sorted(timings.items(), key=operator.itemgetter(1)): 358 | print(f"{cls.__name__:20s}: {avg:8.2f} us ({avg / min_avg:.1f}x)") 359 | 360 | 361 | if __name__ == '__main__': 362 | import sys 363 | 364 | try: 365 | sys.set_int_max_str_digits(len(PI_DIGITS)) 366 | except AttributeError: 367 | pass 368 | 369 | import platform 370 | print(f"Running benchmarks with Python {platform.python_version()} on {platform.platform()}") 371 | 372 | main(*sys.argv[1:]) 373 | -------------------------------------------------------------------------------- /src/quicktions.pyx: -------------------------------------------------------------------------------- 1 | # cython: language_level=3 2 | ## cython: profile=True 3 | # cython: freethreading_compatible=True 4 | 5 | # Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 6 | # 2011, 2012, 2013, 2014 Python Software Foundation; All Rights Reserved 7 | # 8 | # Based on the "fractions" module in CPython 3.4+. 9 | # https://hg.python.org/cpython/file/b18288f24501/Lib/fractions.py 10 | # 11 | # Updated to match the recent development in CPython. 12 | # https://github.com/python/cpython/blob/main/Lib/fractions.py 13 | # 14 | # Adapted for efficient Cython compilation by Stefan Behnel. 15 | # 16 | 17 | """ 18 | Fast fractions data type for rational numbers. 19 | 20 | This is an almost-drop-in replacement for the standard library's 21 | "fractions.Fraction". 22 | """ 23 | 24 | __all__ = ['Fraction'] 25 | 26 | __version__ = '1.22' 27 | 28 | cimport cython 29 | from cpython.unicode cimport Py_UNICODE_TODECIMAL 30 | from cpython.object cimport Py_LT, Py_LE, Py_EQ, Py_NE, Py_GT, Py_GE 31 | from cpython.long cimport PyLong_FromString 32 | 33 | cdef extern from *: 34 | cdef long LONG_MAX, INT_MAX 35 | cdef long long PY_LLONG_MIN, PY_LLONG_MAX 36 | cdef long long MAX_SMALL_NUMBER "(PY_LLONG_MAX / 100)" 37 | 38 | cdef object Rational, Integral, Real, Complex, Decimal, math, operator, re 39 | cdef object PY_MAX_ULONGLONG = ( PY_LLONG_MAX) * 2 + 1 40 | 41 | from numbers import Rational, Integral, Real, Complex 42 | from decimal import Decimal 43 | import math 44 | import operator 45 | import re 46 | 47 | cdef object _operator_index = operator.index 48 | cdef object math_gcd = math.gcd 49 | 50 | # Cache widely used 10**x int objects. 51 | # Py3.12/Ubuntu64: sys.getsizeof(tuple[58]) == 512 bytes, tuple[91] == 768, tuple[123] == 1024 52 | DEF CACHED_POW10 = 91 53 | 54 | cdef tuple _cache_pow10(): 55 | cdef int i 56 | in_ull = True 57 | l = [] 58 | x = 1 59 | for i in range(CACHED_POW10): 60 | l.append(x) 61 | if in_ull: 62 | try: 63 | _C_POW_10[i] = x 64 | except OverflowError: 65 | in_ull = False 66 | x *= 10 67 | return tuple(l) 68 | 69 | cdef unsigned long long[CACHED_POW10] _C_POW_10 70 | cdef tuple POW_10 = _cache_pow10() 71 | 72 | 73 | cdef unsigned long long _c_pow10(Py_ssize_t i): 74 | return _C_POW_10[i] 75 | 76 | 77 | cdef pow10(long long i): 78 | if 0 <= i < CACHED_POW10: 79 | return POW_10[i] 80 | else: 81 | return 10 ** ( i) 82 | 83 | 84 | # Half-private GCD implementation. 85 | 86 | cdef extern from *: 87 | """ 88 | #if defined(__Pyx_PyLong_DigitCount) 89 | #define __Quicktions_HAS_ISLONGLONG (1) 90 | #define __Quicktions_PyLong_IsLongLong(x) (__Pyx_PyLong_DigitCount(x) <= 2) 91 | #else 92 | #define __Quicktions_HAS_ISLONGLONG (0) 93 | #define __Quicktions_PyLong_IsLongLong(x) (0) 94 | #endif 95 | 96 | #if PY_VERSION_HEX >= 0x030c00a5 && defined(PyUnstable_Long_IsCompact) && defined(PyUnstable_Long_CompactValue) 97 | #define __Quicktions_PyLong_IsCompact(x) PyUnstable_Long_IsCompact((PyLongObject*) (x)) 98 | #if CYTHON_COMPILING_IN_CPYTHON 99 | #define __Quicktions_PyLong_CompactValueUnsigned(x) ((unsigned long long) (((PyLongObject*)x)->long_value.ob_digit[0])) 100 | #else 101 | #define __Quicktions_PyLong_CompactValueUnsigned(x) ((unsigned long long) PyUnstable_Long_CompactValue((PyLongObject*) (x)))) 102 | #endif 103 | #elif PY_VERSION_HEX < 0x030c0000 && CYTHON_COMPILING_IN_CPYTHON 104 | #define __Quicktions_PyLong_IsCompact(x) (Py_SIZE(x) == 0 || Py_SIZE(x) == 1 || Py_SIZE(x) == -1) 105 | #define __Quicktions_PyLong_CompactValueUnsigned(x) ((unsigned long long) ((Py_SIZE(x) == 0) ? 0 : (((PyLongObject*)x)->ob_digit)[0])) 106 | #else 107 | #define __Quicktions_PyLong_IsCompact(x) (0) 108 | #define __Quicktions_PyLong_CompactValueUnsigned(x) (0U) 109 | #endif 110 | #if PY_VERSION_HEX >= 0x030d0000 || !CYTHON_COMPILING_IN_CPYTHON 111 | #define _PyLong_GCD(a, b) (NULL) 112 | #endif 113 | 114 | #ifdef __has_builtin 115 | #if __has_builtin(__builtin_ctzg) 116 | #define __Quicktions_HAS_FAST_CTZ_uint (1) 117 | #define __Quicktions_trailing_zeros_uint(x) __builtin_ctzg(x) 118 | #define __Quicktions_HAS_FAST_CTZ_ulong (1) 119 | #define __Quicktions_trailing_zeros_ulong(x) __builtin_ctzg(x) 120 | #define __Quicktions_HAS_FAST_CTZ_ullong (1) 121 | #define __Quicktions_trailing_zeros_ullong(x) __builtin_ctzg(x) 122 | #else 123 | #if __has_builtin(__builtin_ctz) 124 | #define __Quicktions_HAS_FAST_CTZ_uint (1) 125 | #define __Quicktions_trailing_zeros_uint(x) __builtin_ctz(x) 126 | #endif 127 | #if __has_builtin(__builtin_ctzl) 128 | #define __Quicktions_HAS_FAST_CTZ_ulong (1) 129 | #define __Quicktions_trailing_zeros_ulong(x) __builtin_ctzl(x) 130 | #endif 131 | #if __has_builtin(__builtin_ctzll) 132 | #define __Quicktions_HAS_FAST_CTZ_ullong (1) 133 | #define __Quicktions_trailing_zeros_ullong(x) __builtin_ctzll(x) 134 | #endif 135 | #endif 136 | #elif defined(_MSC_VER) && SIZEOF_INT == 4 137 | /* Typical Windows64 config (Win32 does not define "_BitScanForward64"). */ 138 | #define __Quicktions_HAS_FAST_CTZ_uint (1) 139 | #pragma intrinsic(_BitScanForward) 140 | static CYTHON_INLINE int __Quicktions_trailing_zeros_uint(uint32_t x) { 141 | unsigned long bits; 142 | _BitScanForward(&bits, x); 143 | return (int) bits; 144 | } 145 | #if SIZEOF_LONG == 4 146 | #define __Quicktions_HAS_FAST_CTZ_ulong (1) 147 | #define __Quicktions_trailing_zeros_ulong(x) __Quicktions_trailing_zeros_uint(x) 148 | #endif 149 | 150 | /* Win32 does not define "_BitScanForward64". */ 151 | #if defined(_WIN64) && SIZEOF_LONG_LONG == 8 152 | #define __Quicktions_HAS_FAST_CTZ_ullong (1) 153 | #pragma intrinsic(_BitScanForward64) 154 | static CYTHON_INLINE int __Quicktions_trailing_zeros_ullong(uint64_t x) { 155 | unsigned long bits; 156 | _BitScanForward64(&bits, x); 157 | return (int) bits; 158 | } 159 | #if SIZEOF_LONG == 8 160 | #define __Quicktions_HAS_FAST_CTZ_ulong (1) 161 | #define __Quicktions_trailing_zeros_ulong(x) __Quicktions_trailing_zeros_ullong(x) 162 | #endif 163 | #endif 164 | #endif 165 | 166 | #if !defined(__Quicktions_HAS_FAST_CTZ_uint) 167 | #define __Quicktions_HAS_FAST_CTZ_uint (0) 168 | #define __Quicktions_trailing_zeros_uint(x) (0) 169 | #endif 170 | #if !defined(__Quicktions_HAS_FAST_CTZ_ulong) 171 | #define __Quicktions_HAS_FAST_CTZ_ulong (0) 172 | #define __Quicktions_trailing_zeros_ulong(x) (0) 173 | #endif 174 | #if !defined(__Quicktions_HAS_FAST_CTZ_ullong) 175 | #define __Quicktions_HAS_FAST_CTZ_ullong (0) 176 | #define __Quicktions_trailing_zeros_ullong(x) (0) 177 | #endif 178 | """ 179 | bint PyLong_IsCompact "__Quicktions_PyLong_IsCompact" (x) 180 | unsigned long long PyLong_CompactValueUnsigned "__Quicktions_PyLong_CompactValueUnsigned" (x) 181 | const unsigned long long PY_ULLONG_MAX 182 | 183 | bint PyLong_IsLongLong "__Quicktions_PyLong_IsLongLong" (x) 184 | const bint HAS_ISLONGLONG "__Quicktions_HAS_ISLONGLONG" 185 | 186 | # CPython 3.5-3.12 has a fast PyLong GCD implementation that we can use. 187 | # In CPython 3.13, math.gcd() is fast enough to call it directly. 188 | const bint HAS_FAST_MATH_GCD "(PY_VERSION_HEX >= 0x030d0000)" 189 | const bint HAS_OLD_PYLONG_GCD "(CYTHON_COMPILING_IN_CPYTHON && PY_VERSION_HEX < 0x030d0000)" 190 | object _PyLong_GCD(object a, object b) 191 | 192 | const bint HAS_FAST_CTZ_uint "__Quicktions_HAS_FAST_CTZ_uint" 193 | int trailing_zeros_uint "__Quicktions_trailing_zeros_uint" (unsigned int x) 194 | const bint HAS_FAST_CTZ_ulong "__Quicktions_HAS_FAST_CTZ_ulong" 195 | int trailing_zeros_ulong "__Quicktions_trailing_zeros_ulong" (unsigned long x) 196 | const bint HAS_FAST_CTZ_ullong "__Quicktions_HAS_FAST_CTZ_ullong" 197 | int trailing_zeros_ullong "__Quicktions_trailing_zeros_ullong" (unsigned long long x) 198 | 199 | 200 | def _gcd(a, b): 201 | """Calculate the Greatest Common Divisor of a and b as a non-negative number. 202 | """ 203 | if not isinstance(a, int): 204 | raise ValueError(f"Expected int, got {type(a).__name__}") 205 | if not isinstance(b, int): 206 | raise ValueError(f"Expected int, got {type(b).__name__}") 207 | 208 | return _igcd(int(a), int(b)) 209 | 210 | 211 | cdef _igcd(a, b): 212 | if HAS_ISLONGLONG: 213 | if PyLong_IsLongLong(a) and PyLong_IsLongLong(b): 214 | return _c_gcd( a, b) 215 | elif PyLong_IsCompact(a) and PyLong_IsCompact(b): 216 | return _c_gcd(PyLong_CompactValueUnsigned(a), PyLong_CompactValueUnsigned(b)) 217 | 218 | if HAS_FAST_MATH_GCD: 219 | return math_gcd(a, b) 220 | if HAS_OLD_PYLONG_GCD: 221 | return _PyLong_GCD(a, b) 222 | 223 | return _gcd_fallback(int(a), int(b)) 224 | 225 | 226 | ctypedef unsigned long long ullong 227 | ctypedef unsigned long ulong 228 | ctypedef unsigned int uint 229 | 230 | ctypedef fused cunumber: 231 | ullong 232 | ulong 233 | uint 234 | 235 | 236 | cdef ullong _abs(long long x) noexcept: 237 | if x == PY_LLONG_MIN: 238 | return (PY_LLONG_MAX) + 1 239 | return abs(x) 240 | 241 | 242 | @cython.cdivision(True) 243 | cdef cunumber _euclid_gcd(cunumber a, cunumber b) noexcept: 244 | """Euclid's GCD algorithm""" 245 | while b: 246 | a, b = b, a%b 247 | return a 248 | 249 | 250 | cdef inline int trailing_zeros(cunumber x) noexcept: 251 | if cunumber is uint: 252 | return trailing_zeros_uint(x) 253 | elif cunumber is ulong: 254 | return trailing_zeros_ulong(x) 255 | else: 256 | return trailing_zeros_ullong(x) 257 | 258 | 259 | cdef cunumber _binary_gcd(cunumber a, cunumber b) noexcept: 260 | # See https://en.wikipedia.org/wiki/Binary_GCD_algorithm 261 | if not a: 262 | return b 263 | if not b: 264 | return a 265 | 266 | cdef int az = trailing_zeros(a) 267 | a >>= az 268 | cdef int bz = trailing_zeros(b) 269 | b >>= bz 270 | 271 | cdef int shift = min(az, bz) 272 | 273 | while True: 274 | if a > b: 275 | a, b = b, a 276 | b -= a 277 | if not b: 278 | return a << shift 279 | b >>= trailing_zeros(b) 280 | 281 | 282 | @cython.cdivision(True) 283 | cdef cunumber _hybrid_binary_gcd(cunumber a, cunumber b) noexcept: 284 | # See https://lemire.me/blog/2024/04/13/greatest-common-divisor-the-extended-euclidean-algorithm-and-speed/ 285 | if a < b: 286 | a,b = b,a 287 | if not b: 288 | return a 289 | a %= b 290 | if not a: 291 | return b 292 | 293 | cdef int az = trailing_zeros(a) 294 | cdef int bz = trailing_zeros(b) 295 | cdef int shift = min(az, bz) 296 | a >>= az 297 | b >>= bz 298 | 299 | if cunumber is uint: 300 | diff: cython.int 301 | elif cunumber is ulong: 302 | diff: cython.long 303 | else: 304 | diff: cython.longlong 305 | 306 | while True: 307 | diff = a - b 308 | if a > b: 309 | a,b = b,diff 310 | else: 311 | b -= a 312 | if not b: 313 | return a << shift 314 | b >>= trailing_zeros(diff) 315 | 316 | 317 | @cython.cdivision(True) 318 | cdef cunumber _hybrid_binary_gcd2(cunumber a, cunumber b) noexcept: 319 | # See https://en.algorithmica.org/hpc/algorithms/gcd/ 320 | # See https://lemire.me/blog/2024/04/13/greatest-common-divisor-the-extended-euclidean-algorithm-and-speed/ 321 | if a < b: 322 | a,b = b,a 323 | if not b: 324 | return a 325 | a %= b 326 | if not a: 327 | return b 328 | 329 | cdef int az = trailing_zeros(a) 330 | cdef int bz = trailing_zeros(b) 331 | cdef int shift = min(az, bz) 332 | b >>= bz 333 | 334 | cdef cunumber diff 335 | 336 | while a != 0: 337 | a >>= az 338 | diff = b - a if b > a else a - b 339 | if b > a: 340 | b = a 341 | az = trailing_zeros(diff) 342 | a = diff 343 | 344 | return b << shift 345 | 346 | 347 | cdef _py_gcd(ullong a, ullong b): 348 | return _c_gcd(a, b) if b else a 349 | 350 | 351 | cdef ullong _c_gcd_euclid(ullong a, ullong b): 352 | if not b: 353 | return a 354 | if a <= INT_MAX*2+1 and b <= INT_MAX*2+1: 355 | return _euclid_gcd[uint]( a, b) 356 | elif a <= LONG_MAX*2+1 and b <= LONG_MAX*2+1: 357 | return _euclid_gcd[ulong]( a, b) 358 | else: 359 | return _euclid_gcd[ullong](a, b) 360 | 361 | 362 | cdef ullong _c_gcd_binary(ullong a, ullong b): 363 | if not b: 364 | return a 365 | if HAS_FAST_CTZ_uint and a <= INT_MAX*2+1 and b <= INT_MAX*2+1: 366 | return _binary_gcd[uint]( a, b) 367 | elif HAS_FAST_CTZ_ulong and a <= LONG_MAX*2+1 and b <= LONG_MAX*2+1: 368 | return _binary_gcd[ulong]( a, b) 369 | elif HAS_FAST_CTZ_ullong: 370 | return _binary_gcd[ullong](a, b) 371 | else: 372 | return _c_gcd_euclid(a, b) 373 | 374 | 375 | cdef ullong _c_gcd_hybrid2(ullong a, ullong b): 376 | if not b: 377 | return a 378 | if HAS_FAST_CTZ_uint and a <= INT_MAX*2+1 and b <= INT_MAX*2+1: 379 | return _hybrid_binary_gcd2[uint]( a, b) 380 | elif HAS_FAST_CTZ_ulong and a <= LONG_MAX*2+1 and b <= LONG_MAX*2+1: 381 | return _hybrid_binary_gcd2[ulong]( a, b) 382 | elif HAS_FAST_CTZ_ullong: 383 | return _hybrid_binary_gcd2[ullong](a, b) 384 | else: 385 | return _c_gcd_euclid(a, b) 386 | 387 | 388 | cdef ullong _c_gcd_hybrid(ullong a, ullong b): 389 | if not b: 390 | return a 391 | if HAS_FAST_CTZ_uint and a <= INT_MAX and b <= INT_MAX: 392 | return _hybrid_binary_gcd[uint]( a, b) 393 | elif HAS_FAST_CTZ_ulong and a <= LONG_MAX and b <= LONG_MAX: 394 | return _hybrid_binary_gcd[ulong]( a, b) 395 | elif HAS_FAST_CTZ_ullong: 396 | return _hybrid_binary_gcd[ullong](a, b) 397 | else: 398 | return _c_gcd_euclid(a, b) 399 | 400 | 401 | ctypedef ullong (*fast_cgcd)(ullong a, ullong b) 402 | 403 | cdef fast_cgcd _c_gcd = NULL 404 | cdef fast_cgcd _c_gcd_best_hybrid = NULL 405 | 406 | 407 | def use_gcd_impl(name): 408 | """Change the internal GCD implementation. 409 | 410 | 'name' is one of: 'euclid', 'binary', 'hybrid' 411 | """ 412 | if name not in ('euclid', 'binary', 'hybrid'): 413 | raise ValueError(f"{name!r} is not one of: 'euclid', 'binary', 'hybrid'") 414 | 415 | global _c_gcd, GCD_IMPL 416 | 417 | name = str(name) 418 | if name == 'euclid': 419 | _c_gcd = _c_gcd_euclid 420 | elif name == 'binary': 421 | _c_gcd = _c_gcd_binary 422 | else: 423 | _c_gcd = _c_gcd_best_hybrid 424 | GCD_IMPL = name 425 | 426 | 427 | cdef double _measure_gcd_performance(fast_cgcd func): 428 | from time import perf_counter as timer 429 | cdef int i, j, e 430 | 431 | t = timer() 432 | 433 | # Fibonacci numbers: 434 | cdef unsigned long long a = 1, b = 1, c = 2 435 | 436 | for i in range(1, 91): 437 | a, b, c = b, a+b, b+c 438 | if a & 3 == 0: 439 | func(a, b) 440 | func(a, c) 441 | 442 | # Close numbers: 443 | for i in range(1, 20): 444 | func(209865, 209797) 445 | 446 | # 'Parsed' digits with 10^n denominator: 447 | cdef unsigned long long num = 3 448 | cdef unsigned long long denom = 1 449 | cdef unsigned long long n 450 | 451 | for i in range(1, 18): 452 | num = 10 * num + (i & 7) 453 | denom *= 10 454 | n = num 455 | for e in range(3): 456 | n *= 10 457 | for j in range(8): 458 | func(n + j, denom) 459 | 460 | t = timer() - t 461 | 462 | return t 463 | 464 | 465 | cdef int _tune_cgcd(): 466 | # Find fastest GCD implementation for this machine. 467 | t_binary = _measure_gcd_performance(_c_gcd_binary) 468 | t_euclid = _measure_gcd_performance(_c_gcd_euclid) 469 | t_hybrid = _measure_gcd_performance(_c_gcd_hybrid) 470 | t_hybrid2 = _measure_gcd_performance(_c_gcd_hybrid2) 471 | 472 | name = 'euclid' 473 | best_time = t_euclid 474 | 475 | if t_binary < best_time: 476 | name = 'binary' 477 | best_time = t_binary 478 | 479 | if t_hybrid < best_time: 480 | name = 'hybrid' 481 | best_time = t_hybrid 482 | 483 | if t_hybrid2 < best_time: 484 | name = 'hybrid' 485 | best_time = t_hybrid2 486 | 487 | global _c_gcd_best_hybrid 488 | _c_gcd_best_hybrid = _c_gcd_hybrid if t_hybrid < t_hybrid2 else _c_gcd_hybrid2 489 | 490 | #print(f"E: {t_euclid * 1_000_000:.2f}, B: {t_binary * 1_000_000:.2f}, H1: {t_hybrid * 1_000_000:.2f}, H2: {t_hybrid2 * 1_000_000:.2f}") 491 | use_gcd_impl(name) 492 | 493 | return 0 494 | 495 | _tune_cgcd() 496 | 497 | 498 | cdef _gcd_fallback(a: int, b: int): 499 | """Fallback GCD implementation if _PyLong_GCD() is not available. 500 | """ 501 | # Try doing the computation in C space. If the numbers are too 502 | # large at the beginning, do object calculations until they are small enough. 503 | cdef ullong au, bu 504 | cdef long long ai, bi 505 | 506 | if HAS_ISLONGLONG: 507 | if PyLong_IsLongLong(a) and PyLong_IsLongLong(b): 508 | ai, bi = a, b 509 | au = _abs(ai) 510 | bu = _abs(bi) 511 | return _py_gcd(au, bu) 512 | else: 513 | # Optimistically try to switch to C space. 514 | try: 515 | ai, bi = a, b 516 | except OverflowError: 517 | pass 518 | else: 519 | au = _abs(ai) 520 | bu = _abs(bi) 521 | return _py_gcd(au, bu) 522 | 523 | # Do object calculation until we reach the C space limit. 524 | a = abs(a) 525 | b = abs(b) 526 | while b > PY_MAX_ULONGLONG: 527 | a, b = b, a%b 528 | while b and a > PY_MAX_ULONGLONG: 529 | a, b = b, a%b 530 | if not b: 531 | return a 532 | return _py_gcd(a, b) 533 | 534 | 535 | # Constants related to the hash implementation; hash(x) is based 536 | # on the reduction of x modulo the prime _PyHASH_MODULUS. 537 | 538 | from sys import hash_info 539 | 540 | cdef Py_hash_t _PyHASH_MODULUS = hash_info.modulus 541 | 542 | # Value to be used for rationals that reduce to infinity modulo 543 | # _PyHASH_MODULUS. 544 | cdef Py_hash_t _PyHASH_INF = hash_info.inf 545 | 546 | del hash_info 547 | 548 | 549 | # Helpers for formatting 550 | 551 | cdef _round_to_exponent(n, d, exponent, bint no_neg_zero=False): 552 | """Round a rational number to the nearest multiple of a given power of 10. 553 | 554 | Rounds the rational number n/d to the nearest integer multiple of 555 | 10**exponent, rounding to the nearest even integer multiple in the case of 556 | a tie. Returns a pair (sign: bool, significand: int) representing the 557 | rounded value (-1)**sign * significand * 10**exponent. 558 | 559 | If no_neg_zero is true, then the returned sign will always be False when 560 | the significand is zero. Otherwise, the sign reflects the sign of the 561 | input. 562 | 563 | d must be positive, but n and d need not be relatively prime. 564 | """ 565 | if exponent >= 0: 566 | d *= 10**exponent 567 | else: 568 | n *= 10**-exponent 569 | 570 | # The divmod quotient is correct for round-ties-towards-positive-infinity; 571 | # In the case of a tie, we zero out the least significant bit of q. 572 | q, r = divmod(n + (d >> 1), d) 573 | if r == 0 and d & 1 == 0: 574 | q &= -2 575 | 576 | cdef bint sign = q < 0 if no_neg_zero else n < 0 577 | return sign, abs(q) 578 | 579 | 580 | cdef _round_to_figures(n, d, Py_ssize_t figures): 581 | """Round a rational number to a given number of significant figures. 582 | 583 | Rounds the rational number n/d to the given number of significant figures 584 | using the round-ties-to-even rule, and returns a triple 585 | (sign: bool, significand: int, exponent: int) representing the rounded 586 | value (-1)**sign * significand * 10**exponent. 587 | 588 | In the special case where n = 0, returns a significand of zero and 589 | an exponent of 1 - figures, for compatibility with formatting. 590 | Otherwise, the returned significand satisfies 591 | 10**(figures - 1) <= significand < 10**figures. 592 | 593 | d must be positive, but n and d need not be relatively prime. 594 | figures must be positive. 595 | """ 596 | # Special case for n == 0. 597 | if n == 0: 598 | return False, 0, 1 - figures 599 | 600 | cdef bint sign 601 | 602 | # Find integer m satisfying 10**(m - 1) <= abs(n)/d <= 10**m. (If abs(n)/d 603 | # is a power of 10, either of the two possible values for m is fine.) 604 | str_n, str_d = str(abs(n)), str(d) 605 | cdef Py_ssize_t m = len(str_n) - len(str_d) + (str_d <= str_n) 606 | 607 | # Round to a multiple of 10**(m - figures). The significand we get 608 | # satisfies 10**(figures - 1) <= significand <= 10**figures. 609 | exponent = m - figures 610 | sign, significand = _round_to_exponent(n, d, exponent) 611 | 612 | # Adjust in the case where significand == 10**figures, to ensure that 613 | # 10**(figures - 1) <= significand < 10**figures. 614 | if len(str(significand)) == figures + 1: 615 | significand //= 10 616 | exponent += 1 617 | 618 | return sign, significand, exponent 619 | 620 | 621 | # Pattern for matching non-float-style format specifications. 622 | cdef object _GENERAL_FORMAT_SPECIFICATION_MATCHER = re.compile(r""" 623 | (?: 624 | (?P.)? 625 | (?P[<>=^]) 626 | )? 627 | (?P[-+ ]?) 628 | # Alt flag forces a slash and denominator in the output, even for 629 | # integer-valued Fraction objects. 630 | (?P\#)? 631 | # We don't implement the zeropad flag since there's no single obvious way 632 | # to interpret it. 633 | (?P0|[1-9][0-9]*)? 634 | (?P[,_])? 635 | $ 636 | """, re.DOTALL | re.VERBOSE).match 637 | 638 | 639 | # Pattern for matching float-style format specifications; 640 | # supports 'e', 'E', 'f', 'F', 'g', 'G' and '%' presentation types. 641 | cdef object _FLOAT_FORMAT_SPECIFICATION_MATCHER = re.compile(r""" 642 | (?: 643 | (?P.)? 644 | (?P[<>=^]) 645 | )? 646 | (?P[-+ ]?) 647 | (?Pz)? 648 | (?P\#)? 649 | # A '0' that's *not* followed by another digit is parsed as a minimum width 650 | # rather than a zeropad flag. 651 | (?P0(?=[0-9]))? 652 | (?P[0-9]+)? 653 | (?P[,_])? 654 | (?:\. 655 | (?=[,_0-9]) # lookahead for digit or separator 656 | (?P[0-9]+)? 657 | (?P[,_])? 658 | )? 659 | (?P[eEfFgG%]) 660 | $ 661 | """, re.DOTALL | re.VERBOSE).match 662 | 663 | cdef object NOINIT = object() 664 | 665 | 666 | cdef class Fraction: 667 | """A Rational number. 668 | 669 | Takes a string like '3/2' or '1.5', another Rational instance, a 670 | numerator/denominator pair, or a float. 671 | 672 | Examples 673 | -------- 674 | 675 | >>> Fraction(10, -8) 676 | Fraction(-5, 4) 677 | >>> Fraction(Fraction(1, 7), 5) 678 | Fraction(1, 35) 679 | >>> Fraction(Fraction(1, 7), Fraction(2, 3)) 680 | Fraction(3, 14) 681 | >>> Fraction('314') 682 | Fraction(314, 1) 683 | >>> Fraction('-35/4') 684 | Fraction(-35, 4) 685 | >>> Fraction('3.1415') # conversion from numeric string 686 | Fraction(6283, 2000) 687 | >>> Fraction('-47e-2') # string may include a decimal exponent 688 | Fraction(-47, 100) 689 | >>> Fraction(1.47) # direct construction from float (exact conversion) 690 | Fraction(6620291452234629, 4503599627370496) 691 | >>> Fraction(2.25) 692 | Fraction(9, 4) 693 | >>> from decimal import Decimal 694 | >>> Fraction(Decimal('1.47')) 695 | Fraction(147, 100) 696 | 697 | """ 698 | cdef _numerator 699 | cdef _denominator 700 | cdef Py_hash_t _hash 701 | 702 | def __cinit__(self, numerator=0, denominator=None): 703 | self._hash = -1 704 | if numerator is NOINIT: 705 | return # fast-path for external initialisation 706 | 707 | cdef bint _normalize = True 708 | if denominator is None: 709 | if type(numerator) is int: 710 | self._numerator = numerator 711 | self._denominator = 1 712 | return 713 | 714 | elif type(numerator) is float: 715 | # Exact conversion 716 | self._numerator, self._denominator = numerator.as_integer_ratio() 717 | return 718 | 719 | elif type(numerator) is Fraction: 720 | self._numerator = (numerator)._numerator 721 | self._denominator = (numerator)._denominator 722 | return 723 | 724 | elif isinstance(numerator, unicode): 725 | numerator, denominator, is_normalised = _parse_fraction[unicode]( 726 | numerator, len(numerator), numerator) 727 | if is_normalised: 728 | _normalize = False 729 | # fall through to normalisation below 730 | 731 | elif isinstance(numerator, float) or ( 732 | not isinstance(numerator, type) and hasattr(numerator, 'as_integer_ratio')): 733 | # Exact conversion 734 | self._numerator, self._denominator = numerator.as_integer_ratio() 735 | return 736 | 737 | elif isinstance(numerator, (Fraction, Rational)): 738 | self._numerator = numerator.numerator 739 | self._denominator = numerator.denominator 740 | return 741 | 742 | elif isinstance(numerator, Decimal): 743 | self._numerator, self._denominator = numerator.as_integer_ratio() 744 | return 745 | 746 | else: 747 | raise TypeError("argument should be a string or a Rational " 748 | "instance or have the as_integer_ratio() method") 749 | 750 | elif type(numerator) is int is type(denominator): 751 | pass # *very* normal case 752 | 753 | elif type(numerator) is Fraction is type(denominator): 754 | numerator, denominator = ( 755 | (numerator)._numerator * (denominator)._denominator, 756 | (denominator)._numerator * (numerator)._denominator 757 | ) 758 | 759 | elif (isinstance(numerator, (Fraction, Rational)) and 760 | isinstance(denominator, (Fraction, Rational))): 761 | numerator, denominator = ( 762 | numerator.numerator * denominator.denominator, 763 | denominator.numerator * numerator.denominator 764 | ) 765 | 766 | else: 767 | raise TypeError("both arguments should be " 768 | "Rational instances") 769 | 770 | if denominator == 0: 771 | raise ZeroDivisionError(f'Fraction({numerator}, 0)') 772 | if _normalize: 773 | if not isinstance(numerator, int): 774 | numerator = int(numerator) 775 | if not isinstance(denominator, int): 776 | denominator = int(denominator) 777 | g = _igcd(numerator, denominator) 778 | # NOTE: 'is' tests on integers are generally a bad idea, but 779 | # they are fast and if they fail here, it'll still be correct 780 | if denominator < 0: 781 | if g is 1: 782 | numerator = -numerator 783 | denominator = -denominator 784 | else: 785 | g = -g 786 | if g is not 1: 787 | numerator //= g 788 | denominator //= g 789 | self._numerator = numerator 790 | self._denominator = denominator 791 | 792 | @classmethod 793 | def from_number(cls, number): 794 | """Converts a finite real number to a rational number, exactly. 795 | 796 | Beware that Fraction.from_number(0.3) != Fraction(3, 10). 797 | 798 | """ 799 | if type(number) is Fraction: 800 | return _fraction_from_coprime_ints(( number)._numerator, ( number)._denominator, cls) 801 | 802 | elif isinstance(number, int): 803 | return _fraction_from_coprime_ints(number, 1, cls) 804 | 805 | elif isinstance(number, float): 806 | n, d = number.as_integer_ratio() 807 | return _fraction_from_coprime_ints(n, d, cls) 808 | 809 | elif isinstance(number, Rational): 810 | return _fraction_from_coprime_ints(number.numerator, number.denominator, cls) 811 | 812 | elif not isinstance(number, type) and hasattr(number, 'as_integer_ratio'): 813 | n, d = number.as_integer_ratio() 814 | return _fraction_from_coprime_ints(n, d, cls) 815 | 816 | else: 817 | raise TypeError("argument should be a Rational instance or " 818 | "have the as_integer_ratio() method") 819 | 820 | @classmethod 821 | def from_float(cls, f): 822 | """Converts a finite float to a rational number, exactly. 823 | 824 | Beware that Fraction.from_float(0.3) != Fraction(3, 10). 825 | 826 | """ 827 | try: 828 | ratio = f.as_integer_ratio() 829 | except (ValueError, OverflowError, AttributeError): 830 | pass # not something we can convert, raise concrete exceptions below 831 | else: 832 | return cls(*ratio) 833 | 834 | if isinstance(f, Integral): 835 | return cls(f) 836 | elif not isinstance(f, float): 837 | raise TypeError(f"{cls.__name__}.from_float() only takes floats, not {f!r} ({type(f).__name__})") 838 | if math.isinf(f): 839 | raise OverflowError(f"Cannot convert {f!r} to {cls.__name__}.") 840 | raise ValueError(f"Cannot convert {f!r} to {cls.__name__}.") 841 | 842 | @classmethod 843 | def from_decimal(cls, dec): 844 | """Converts a finite Decimal instance to a rational number, exactly.""" 845 | cdef Py_ssize_t exp 846 | if isinstance(dec, Integral): 847 | dec = Decimal(int(dec)) 848 | elif not isinstance(dec, Decimal): 849 | raise TypeError( 850 | f"{cls.__name__}.from_decimal() only takes Decimals, not {dec!r} ({type(dec).__name__})") 851 | if dec.is_infinite(): 852 | raise OverflowError(f"Cannot convert {dec} to {cls.__name__}.") 853 | if dec.is_nan(): 854 | raise ValueError(f"Cannot convert {dec} to {cls.__name__}.") 855 | 856 | num, denom = dec.as_integer_ratio() 857 | return _fraction_from_coprime_ints(num, denom, cls) 858 | 859 | def is_integer(self): 860 | """Return True if the Fraction is an integer.""" 861 | return self._denominator == 1 862 | 863 | def as_integer_ratio(self): 864 | """Return a pair of integers, whose ratio is equal to the original Fraction. 865 | 866 | The ratio is in lowest terms and has a positive denominator. 867 | """ 868 | return (self._numerator, self._denominator) 869 | 870 | def limit_denominator(self, max_denominator=1000000): 871 | """Closest Fraction to self with denominator at most max_denominator. 872 | 873 | >>> Fraction('3.141592653589793').limit_denominator(10) 874 | Fraction(22, 7) 875 | >>> Fraction('3.141592653589793').limit_denominator(100) 876 | Fraction(311, 99) 877 | >>> Fraction(4321, 8765).limit_denominator(10000) 878 | Fraction(4321, 8765) 879 | 880 | """ 881 | # Algorithm notes: For any real number x, define a *best upper 882 | # approximation* to x to be a rational number p/q such that: 883 | # 884 | # (1) p/q >= x, and 885 | # (2) if p/q > r/s >= x then s > q, for any rational r/s. 886 | # 887 | # Define *best lower approximation* similarly. Then it can be 888 | # proved that a rational number is a best upper or lower 889 | # approximation to x if, and only if, it is a convergent or 890 | # semiconvergent of the (unique shortest) continued fraction 891 | # associated to x. 892 | # 893 | # To find a best rational approximation with denominator <= M, 894 | # we find the best upper and lower approximations with 895 | # denominator <= M and take whichever of these is closer to x. 896 | # In the event of a tie, the bound with smaller denominator is 897 | # chosen. If both denominators are equal (which can happen 898 | # only when max_denominator == 1 and self is midway between 899 | # two integers) the lower bound---i.e., the floor of self, is 900 | # taken. 901 | 902 | if max_denominator < 1: 903 | raise ValueError("max_denominator should be at least 1") 904 | if self._denominator <= max_denominator: 905 | return Fraction(self) 906 | 907 | p0, q0, p1, q1 = 0, 1, 1, 0 908 | n, d = self._numerator, self._denominator 909 | while True: 910 | a = n//d 911 | q2 = q0+a*q1 912 | if q2 > max_denominator: 913 | break 914 | p0, q0, p1, q1 = p1, q1, p0+a*p1, q2 915 | n, d = d, n-a*d 916 | 917 | k = (max_denominator-q0)//q1 918 | 919 | # Determine which of the candidates (p0+k*p1)/(q0+k*q1) and p1/q1 is 920 | # closer to self. The distance between them is 1/(q1*(q0+k*q1)), while 921 | # the distance from p1/q1 to self is d/(q1*self._denominator). So we 922 | # need to compare 2*(q0+k*q1) with self._denominator/d. 923 | if 2*d*(q0+k*q1) <= self._denominator: 924 | return _fraction_from_coprime_ints(p1, q1) 925 | else: 926 | return _fraction_from_coprime_ints(p0+k*p1, q0+k*q1) 927 | 928 | @property 929 | def numerator(self): 930 | return self._numerator 931 | 932 | @property 933 | def denominator(self): 934 | return self._denominator 935 | 936 | def __repr__(self): 937 | """repr(self)""" 938 | return '%s(%s, %s)' % (self.__class__.__name__, 939 | self._numerator, self._denominator) 940 | 941 | def __str__(self): 942 | """str(self)""" 943 | if self._denominator == 1: 944 | return str(self._numerator) 945 | else: 946 | return f'{self._numerator}/{self._denominator}' 947 | 948 | @cython.final 949 | cdef _format_general(self, dict match): 950 | """Helper method for __format__. 951 | 952 | Handles fill, alignment, signs, and thousands separators in the 953 | case of no presentation type. 954 | """ 955 | # Validate and parse the format specifier. 956 | fill = match["fill"] or " " 957 | cdef Py_UCS4 align = ord(match["align"] or ">") 958 | pos_sign = "" if match["sign"] == "-" else match["sign"] 959 | cdef bint alternate_form = match["alt"] 960 | cdef Py_ssize_t minimumwidth = int(match["minimumwidth"] or "0") 961 | thousands_sep = match["thousands_sep"] or '' 962 | 963 | cdef Py_ssize_t first_pos # Py2/3.5-only 964 | 965 | # Determine the body and sign representation. 966 | n, d = self._numerator, self._denominator 967 | if d > 1 or alternate_form: 968 | body = f"{abs(n):{thousands_sep}}/{d:{thousands_sep}}" 969 | else: 970 | body = f"{abs(n):{thousands_sep}}" 971 | sign = '-' if n < 0 else pos_sign 972 | 973 | # Pad with fill character if necessary and return. 974 | padding = fill * (minimumwidth - len(sign) - len(body)) 975 | if align == u">": 976 | return padding + sign + body 977 | elif align == u"<": 978 | return sign + body + padding 979 | elif align == u"^": 980 | half = len(padding) // 2 981 | return padding[:half] + sign + body + padding[half:] 982 | else: # align == u"=" 983 | return sign + padding + body 984 | 985 | @cython.final 986 | cdef _format_float_style(self, dict match): 987 | """Helper method for __format__; handles float presentation types.""" 988 | fill = match["fill"] or " " 989 | align = match["align"] or ">" 990 | pos_sign = "" if match["sign"] == "-" else match["sign"] 991 | cdef bint no_neg_zero = match["no_neg_zero"] 992 | cdef bint alternate_form = match["alt"] 993 | cdef bint zeropad = match["zeropad"] 994 | cdef Py_ssize_t minimumwidth = int(match["minimumwidth"] or "0") 995 | thousands_sep = match["thousands_sep"] 996 | cdef Py_ssize_t precision = int(match["precision"] or "6") 997 | cdef str frac_sep = match["frac_separators"] or "" 998 | cdef Py_UCS4 presentation_type = ord(match["presentation_type"]) 999 | cdef bint trim_zeros = presentation_type in u"gG" and not alternate_form 1000 | cdef bint trim_point = not alternate_form 1001 | exponent_indicator = "E" if presentation_type in u"EFG" else "e" 1002 | 1003 | if align == '=' and fill == '0': 1004 | zeropad = True 1005 | 1006 | cdef bint negative, scientific 1007 | cdef Py_ssize_t exponent, figures 1008 | 1009 | # Round to get the digits we need, figure out where to place the point, 1010 | # and decide whether to use scientific notation. 'point_pos' is the 1011 | # relative to the _end_ of the digit string: that is, it's the number 1012 | # of digits that should follow the point. 1013 | if presentation_type in u"fF%": 1014 | exponent = -precision 1015 | if presentation_type == u"%": 1016 | exponent -= 2 1017 | negative, significand = _round_to_exponent( 1018 | self._numerator, self._denominator, exponent, no_neg_zero) 1019 | scientific = False 1020 | point_pos = precision 1021 | else: # presentation_type in "eEgG" 1022 | figures = ( 1023 | max(precision, 1) 1024 | if presentation_type in u"gG" 1025 | else precision + 1 1026 | ) 1027 | negative, significand, exponent = _round_to_figures( 1028 | self._numerator, self._denominator, figures) 1029 | scientific = ( 1030 | presentation_type in u"eE" 1031 | or exponent > 0 1032 | or exponent + figures <= -4 1033 | ) 1034 | point_pos = figures - 1 if scientific else -exponent 1035 | 1036 | # Get the suffix - the part following the digits, if any. 1037 | if presentation_type == u"%": 1038 | suffix = "%" 1039 | elif scientific: 1040 | #suffix = f"{exponent_indicator}{exponent + point_pos:+03d}" 1041 | suffix = "%s%+03d" % (exponent_indicator, exponent + point_pos) 1042 | else: 1043 | suffix = "" 1044 | 1045 | # String of output digits, padded sufficiently with zeros on the left 1046 | # so that we'll have at least one digit before the decimal point. 1047 | digits = f"{significand:0{point_pos + 1}d}" 1048 | 1049 | # Before padding, the output has the form f"{sign}{leading}{trailing}", 1050 | # where `leading` includes thousands separators if necessary and 1051 | # `trailing` includes the decimal separator where appropriate. 1052 | sign = "-" if negative else pos_sign 1053 | leading = digits[: len(digits) - point_pos] 1054 | frac_part = digits[len(digits) - point_pos :] 1055 | if trim_zeros: 1056 | frac_part = frac_part.rstrip("0") 1057 | separator = "" if trim_point and not frac_part else "." 1058 | if frac_sep: 1059 | frac_part = frac_sep.join([ 1060 | frac_part[pos:pos + 3] for pos in range(0, len(frac_part), 3) 1061 | ]) 1062 | trailing = separator + frac_part + suffix 1063 | 1064 | # Do zero padding if required. 1065 | if zeropad: 1066 | min_leading = minimumwidth - len(sign) - len(trailing) 1067 | # When adding thousands separators, they'll be added to the 1068 | # zero-padded portion too, so we need to compensate. 1069 | leading = leading.zfill( 1070 | 3 * min_leading // 4 + 1 if thousands_sep else min_leading 1071 | ) 1072 | 1073 | # Insert thousands separators if required. 1074 | if thousands_sep: 1075 | first_pos = 1 + (len(leading) - 1) % 3 1076 | leading = leading[:first_pos] + "".join([ 1077 | thousands_sep + leading[pos : pos + 3] 1078 | for pos in range(first_pos, len(leading), 3) 1079 | ]) 1080 | 1081 | # We now have a sign and a body. Pad with fill character if necessary 1082 | # and return. 1083 | body = leading + trailing 1084 | padding = fill * (minimumwidth - len(sign) - len(body)) 1085 | if align == ">": 1086 | return padding + sign + body 1087 | elif align == "<": 1088 | return sign + body + padding 1089 | elif align == "^": 1090 | half = len(padding) // 2 1091 | return padding[:half] + sign + body + padding[half:] 1092 | else: # align == "=" 1093 | return sign + padding + body 1094 | 1095 | def __format__(self, format_spec, /): 1096 | """Format this fraction according to the given format specification.""" 1097 | 1098 | if match := _GENERAL_FORMAT_SPECIFICATION_MATCHER(format_spec): 1099 | return self._format_general(match.groupdict()) 1100 | 1101 | if match := _FLOAT_FORMAT_SPECIFICATION_MATCHER(format_spec): 1102 | # Refuse the temptation to guess if both alignment _and_ 1103 | # zero padding are specified. 1104 | match_groups = match.groupdict() 1105 | if match_groups["align"] is None or match_groups["zeropad"] is None: 1106 | return self._format_float_style(match_groups) 1107 | 1108 | raise ValueError( 1109 | f"Invalid format specifier {format_spec!r} " 1110 | f"for object of type {type(self).__name__!r}" 1111 | ) 1112 | 1113 | def __add__(a, b): 1114 | """a + b""" 1115 | return forward(a, b, _add, _math_op_add) 1116 | 1117 | def __radd__(b, a): 1118 | """a + b""" 1119 | return reverse(a, b, _add, _math_op_add) 1120 | 1121 | def __sub__(a, b): 1122 | """a - b""" 1123 | return forward(a, b, _sub, _math_op_sub) 1124 | 1125 | def __rsub__(b, a): 1126 | """a - b""" 1127 | return reverse(a, b, _sub, _math_op_sub) 1128 | 1129 | def __mul__(a, b): 1130 | """a * b""" 1131 | return forward(a, b, _mul, _math_op_mul) 1132 | 1133 | def __rmul__(b, a): 1134 | """a * b""" 1135 | return reverse(a, b, _mul, _math_op_mul) 1136 | 1137 | def __div__(a, b): 1138 | """a / b""" 1139 | return forward(a, b, _div, _math_op_div) 1140 | 1141 | def __rdiv__(b, a): 1142 | """a / b""" 1143 | return reverse(a, b, _div, _math_op_div) 1144 | 1145 | def __truediv__(a, b): 1146 | """a / b""" 1147 | return forward(a, b, _div, _math_op_truediv) 1148 | 1149 | def __rtruediv__(b, a): 1150 | """a / b""" 1151 | return reverse(a, b, _div, _math_op_truediv) 1152 | 1153 | def __floordiv__(a, b): 1154 | """a // b""" 1155 | return forward(a, b, _floordiv, _math_op_floordiv, handle_complex=False) 1156 | 1157 | def __rfloordiv__(b, a): 1158 | """a // b""" 1159 | return reverse(a, b, _floordiv, _math_op_floordiv, handle_complex=False) 1160 | 1161 | def __mod__(a, b): 1162 | """a % b""" 1163 | return forward(a, b, _mod, _math_op_mod, handle_complex=False) 1164 | 1165 | def __rmod__(b, a): 1166 | """a % b""" 1167 | return reverse(a, b, _mod, _math_op_mod, handle_complex=False) 1168 | 1169 | def __divmod__(a, b): 1170 | """divmod(self, other): The pair (self // other, self % other). 1171 | 1172 | Sometimes this can be computed faster than the pair of 1173 | operations. 1174 | """ 1175 | return forward(a, b, _divmod, _math_op_divmod, handle_complex=False) 1176 | 1177 | def __rdivmod__(b, a): 1178 | """divmod(self, other): The pair (self // other, self % other). 1179 | 1180 | Sometimes this can be computed faster than the pair of 1181 | operations. 1182 | """ 1183 | return reverse(a, b, _divmod, _math_op_divmod, handle_complex=False) 1184 | 1185 | def __pow__(a, b, x): 1186 | """a ** b 1187 | 1188 | If b is not an integer, the result will be a float or complex 1189 | since roots are generally irrational. If b is an integer, the 1190 | result will be rational. 1191 | """ 1192 | if x is not None: 1193 | return NotImplemented 1194 | 1195 | if isinstance(b, int): 1196 | return _pow(a.numerator, a.denominator, b, 1) 1197 | elif isinstance(b, (Fraction, Rational)): 1198 | return _pow(a.numerator, a.denominator, b.numerator, b.denominator) 1199 | elif isinstance(b, (float, complex)): 1200 | return (a.numerator / a.denominator) ** b 1201 | else: 1202 | return NotImplemented 1203 | 1204 | def __rpow__(b, a, x): 1205 | """a ** b 1206 | 1207 | If b is not an integer, the result will be a float or complex 1208 | since roots are generally irrational. If b is an integer, the 1209 | result will be rational. 1210 | """ 1211 | if x is not None: 1212 | return NotImplemented 1213 | 1214 | bn, bd = b.numerator, b.denominator 1215 | if bd == 1 and bn >= 0: 1216 | # If a is an int, keep it that way if possible. 1217 | return a ** bn 1218 | 1219 | if isinstance(a, int): 1220 | return _pow(a, 1, bn, bd) 1221 | if isinstance(a, (Fraction, Rational)): 1222 | return _pow(a.numerator, a.denominator, bn, bd) 1223 | 1224 | if bd == 1: 1225 | return a ** bn 1226 | 1227 | return a ** (bn / bd) 1228 | 1229 | def __pos__(a): 1230 | """+a: Coerces a subclass instance to Fraction""" 1231 | if type(a) is Fraction: 1232 | return a 1233 | return _fraction_from_coprime_ints(a._numerator, a._denominator) 1234 | 1235 | def __neg__(a): 1236 | """-a""" 1237 | return _fraction_from_coprime_ints(-a._numerator, a._denominator) 1238 | 1239 | def __abs__(a): 1240 | """abs(a)""" 1241 | return _fraction_from_coprime_ints(abs(a._numerator), a._denominator) 1242 | 1243 | def __int__(a): 1244 | """int(a)""" 1245 | if a._numerator < 0: 1246 | return _operator_index(-(-a._numerator // a._denominator)) 1247 | else: 1248 | return _operator_index(a._numerator // a._denominator) 1249 | 1250 | def __trunc__(a): 1251 | """math.trunc(a)""" 1252 | if a._numerator < 0: 1253 | return -(-a._numerator // a._denominator) 1254 | else: 1255 | return a._numerator // a._denominator 1256 | 1257 | def __floor__(a): 1258 | """math.floor(a)""" 1259 | return a.numerator // a.denominator 1260 | 1261 | def __ceil__(a): 1262 | """math.ceil(a)""" 1263 | # The negations cleverly convince floordiv to return the ceiling. 1264 | return -(-a.numerator // a.denominator) 1265 | 1266 | def __round__(self, ndigits=None): 1267 | """round(self, ndigits) 1268 | 1269 | Rounds half toward even. 1270 | """ 1271 | if ndigits is None: 1272 | floor, remainder = divmod(self.numerator, self.denominator) 1273 | if remainder * 2 < self.denominator: 1274 | return floor 1275 | elif remainder * 2 > self.denominator: 1276 | return floor + 1 1277 | # Deal with the half case: 1278 | elif floor % 2 == 0: 1279 | return floor 1280 | else: 1281 | return floor + 1 1282 | shift = pow10(abs(ndigits)) 1283 | # See _operator_fallbacks.forward to check that the results of 1284 | # these operations will always be Fraction and therefore have 1285 | # round(). 1286 | if ndigits > 0: 1287 | return Fraction(round(self * shift), shift) 1288 | else: 1289 | return Fraction(round(self / shift) * shift) 1290 | 1291 | def __float__(self): 1292 | """float(self) = self.numerator / self.denominator 1293 | 1294 | It's important that this conversion use the integer's "true" 1295 | division rather than casting one side to float before dividing 1296 | so that ratios of huge integers convert without overflowing. 1297 | """ 1298 | return _as_float(self.numerator, self.denominator) 1299 | 1300 | # Concrete implementations of Complex abstract methods. 1301 | def __complex__(self): 1302 | """complex(self) == complex(float(self), 0)""" 1303 | return complex(float(self)) 1304 | 1305 | # == +self 1306 | real = property(__pos__, doc="Real numbers are their real component.") 1307 | 1308 | # == 0 1309 | @property 1310 | def imag(self): 1311 | "Real numbers have no imaginary component." 1312 | return 0 1313 | 1314 | def conjugate(self): 1315 | """Conjugate is a no-op for Reals.""" 1316 | return +self 1317 | 1318 | def __hash__(self): 1319 | """hash(self)""" 1320 | if self._hash != -1: 1321 | return self._hash 1322 | 1323 | cdef Py_hash_t result 1324 | 1325 | # In order to make sure that the hash of a Fraction agrees 1326 | # with the hash of a numerically equal integer, float or 1327 | # Decimal instance, we follow the rules for numeric hashes 1328 | # outlined in the documentation. (See library docs, 'Built-in 1329 | # Types'). 1330 | 1331 | try: 1332 | dinv = pow(self._denominator, -1, _PyHASH_MODULUS) 1333 | except ValueError: 1334 | # ValueError means there is no modular inverse. 1335 | result = _PyHASH_INF 1336 | else: 1337 | # The general algorithm now specifies that the absolute value of 1338 | # the hash is 1339 | # (|N| * dinv) % P 1340 | # where N is self._numerator and P is _PyHASH_MODULUS. That's 1341 | # optimized here in two ways: first, for a non-negative int i, 1342 | # hash(i) == i % P, but the int hash implementation doesn't need 1343 | # to divide, and is faster than doing % P explicitly. So we do 1344 | # hash(|N| * dinv) 1345 | # instead. Second, N is unbounded, so its product with dinv may 1346 | # be arbitrarily expensive to compute. The final answer is the 1347 | # same if we use the bounded |N| % P instead, which can again 1348 | # be done with an int hash() call. If 0 <= i < P, hash(i) == i, 1349 | # so this nested hash() call wastes a bit of time making a 1350 | # redundant copy when |N| < P, but can save an arbitrarily large 1351 | # amount of computation for large |N|. 1352 | result = hash(hash(abs(self._numerator)) * dinv) 1353 | 1354 | if self._numerator < 0: 1355 | result = -result 1356 | if result == -1: 1357 | result = -2 1358 | self._hash = result 1359 | return result 1360 | 1361 | def __richcmp__(a, b, int op): 1362 | if isinstance(a, Fraction): 1363 | if op == Py_EQ: 1364 | return (a)._eq(b) 1365 | elif op == Py_NE: 1366 | result = (a)._eq(b) 1367 | return NotImplemented if result is NotImplemented else not result 1368 | else: 1369 | a, b = b, a 1370 | if op == Py_EQ: 1371 | return (a)._eq(b) 1372 | elif op == Py_NE: 1373 | result = (a)._eq(b) 1374 | return NotImplemented if result is NotImplemented else not result 1375 | elif op == Py_LT: 1376 | op = Py_GE 1377 | elif op == Py_GT: 1378 | op = Py_LE 1379 | elif op == Py_LE: 1380 | op = Py_GT 1381 | elif op == Py_GE: 1382 | op = Py_LT 1383 | else: 1384 | return NotImplemented 1385 | return (a)._richcmp(b, op) 1386 | 1387 | @cython.final 1388 | cdef _eq(a, b): 1389 | if type(b) is int: 1390 | return a._numerator == b and a._denominator == 1 1391 | if type(b) is Fraction: 1392 | return (a._numerator == (b)._numerator and 1393 | a._denominator == (b)._denominator) 1394 | if isinstance(b, Rational): 1395 | return (a._numerator == b.numerator and 1396 | a._denominator == b.denominator) 1397 | if isinstance(b, Complex) and b.imag == 0: 1398 | b = b.real 1399 | if isinstance(b, float): 1400 | if math.isnan(b) or math.isinf(b): 1401 | # comparisons with an infinity or nan should behave in 1402 | # the same way for any finite a, so treat a as zero. 1403 | return 0.0 == b 1404 | else: 1405 | return a == a.from_float(b) 1406 | return NotImplemented 1407 | 1408 | @cython.final 1409 | cdef _richcmp(self, other, int op): 1410 | """Helper for comparison operators, for internal use only. 1411 | 1412 | Implement comparison between a Rational instance `self`, and 1413 | either another Rational instance or a float `other`. If 1414 | `other` is not a Rational instance or a float, return 1415 | NotImplemented. `op` should be one of the six standard 1416 | comparison operators. 1417 | 1418 | """ 1419 | # convert other to a Rational instance where reasonable. 1420 | if isinstance(other, int): 1421 | a = self._numerator 1422 | b = self._denominator * other 1423 | elif type(other) is Fraction: 1424 | a = self._numerator * (other)._denominator 1425 | b = self._denominator * (other)._numerator 1426 | elif isinstance(other, float): 1427 | if math.isnan(other) or math.isinf(other): 1428 | a, b = 0.0, other # Comparison to 0.0 is just as good as any. 1429 | else: 1430 | return self._richcmp(self.from_float(other), op) 1431 | elif isinstance(other, (Fraction, Rational)): 1432 | a = self._numerator * other.denominator 1433 | b = self._denominator * other.numerator 1434 | else: 1435 | # comparisons with complex should raise a TypeError, for consistency 1436 | # with int<->complex, float<->complex, and complex<->complex comparisons. 1437 | return NotImplemented 1438 | 1439 | if op == Py_LT: 1440 | return a < b 1441 | elif op == Py_GT: 1442 | return a > b 1443 | elif op == Py_LE: 1444 | return a <= b 1445 | elif op == Py_GE: 1446 | return a >= b 1447 | else: 1448 | return NotImplemented 1449 | 1450 | def __bool__(self): 1451 | """a != 0""" 1452 | # Use bool() because (a._numerator != 0) can return an 1453 | # object which is not a bool. 1454 | # See https://bugs.python.org/issue39274 1455 | return bool(self._numerator) 1456 | 1457 | # support for pickling, copy, and deepcopy 1458 | 1459 | def __reduce__(self): 1460 | return (type(self), (self._numerator, self._denominator)) 1461 | 1462 | def __copy__(self): 1463 | if type(self) is Fraction: 1464 | return self # I'm immutable; therefore I am my own clone 1465 | return type(self)(self._numerator, self._denominator) 1466 | 1467 | def __deepcopy__(self, memo): 1468 | if type(self) is Fraction: 1469 | return self # My components are also immutable 1470 | return type(self)(self._numerator, self._denominator) 1471 | 1472 | 1473 | # Register with Python's numerical tower. 1474 | Rational.register(Fraction) 1475 | 1476 | 1477 | cdef _fraction_from_coprime_ints(numerator, denominator, cls=None): 1478 | """Convert a pair of ints to a rational number, for internal use. 1479 | 1480 | The ratio of integers should be in lowest terms and the denominator 1481 | should be positive. 1482 | """ 1483 | cdef Fraction obj 1484 | if cls is None or cls is Fraction: 1485 | obj = Fraction.__new__(Fraction, NOINIT, NOINIT) 1486 | else: 1487 | obj = cls.__new__(cls) 1488 | obj._numerator = numerator 1489 | obj._denominator = denominator 1490 | return obj 1491 | 1492 | 1493 | cdef _pow(an, ad, bn, bd): 1494 | if bd == 1: 1495 | # power = bn 1496 | if bn >= 0: 1497 | return _fraction_from_coprime_ints( 1498 | an ** bn, 1499 | ad ** bn) 1500 | elif an > 0: 1501 | return _fraction_from_coprime_ints( 1502 | ad ** -bn, 1503 | an ** -bn) 1504 | elif an == 0: 1505 | raise ZeroDivisionError(f'Fraction({ad ** -bn}, 0)') 1506 | else: 1507 | return _fraction_from_coprime_ints( 1508 | (-ad) ** -bn, 1509 | (-an) ** -bn) 1510 | else: 1511 | # A fractional power will generally produce an 1512 | # irrational number. 1513 | return (an / ad) ** (bn / bd) 1514 | 1515 | 1516 | cdef _as_float(numerator, denominator): 1517 | return numerator / denominator 1518 | 1519 | 1520 | # Rational arithmetic algorithms: Knuth, TAOCP, Volume 2, 4.5.1. 1521 | # 1522 | # Assume input fractions a and b are normalized. 1523 | # 1524 | # 1) Consider addition/subtraction. 1525 | # 1526 | # Let g = gcd(da, db). Then 1527 | # 1528 | # na nb na*db ± nb*da 1529 | # a ± b == -- ± -- == ------------- == 1530 | # da db da*db 1531 | # 1532 | # na*(db//g) ± nb*(da//g) t 1533 | # == ----------------------- == - 1534 | # (da*db)//g d 1535 | # 1536 | # Now, if g > 1, we're working with smaller integers. 1537 | # 1538 | # Note, that t, (da//g) and (db//g) are pairwise coprime. 1539 | # 1540 | # Indeed, (da//g) and (db//g) share no common factors (they were 1541 | # removed) and da is coprime with na (since input fractions are 1542 | # normalized), hence (da//g) and na are coprime. By symmetry, 1543 | # (db//g) and nb are coprime too. Then, 1544 | # 1545 | # gcd(t, da//g) == gcd(na*(db//g), da//g) == 1 1546 | # gcd(t, db//g) == gcd(nb*(da//g), db//g) == 1 1547 | # 1548 | # Above allows us optimize reduction of the result to lowest 1549 | # terms. Indeed, 1550 | # 1551 | # g2 = gcd(t, d) == gcd(t, (da//g)*(db//g)*g) == gcd(t, g) 1552 | # 1553 | # t//g2 t//g2 1554 | # a ± b == ----------------------- == ---------------- 1555 | # (da//g)*(db//g)*(g//g2) (da//g)*(db//g2) 1556 | # 1557 | # is a normalized fraction. This is useful because the unnormalized 1558 | # denominator d could be much larger than g. 1559 | # 1560 | # We should special-case g == 1 (and g2 == 1), since 60.8% of 1561 | # randomly-chosen integers are coprime: 1562 | # https://en.wikipedia.org/wiki/Coprime_integers#Probability_of_coprimality 1563 | # Note, that g2 == 1 always for fractions, obtained from floats: here 1564 | # g is a power of 2 and the unnormalized numerator t is an odd integer. 1565 | # 1566 | # 2) Consider multiplication 1567 | # 1568 | # Let g1 = gcd(na, db) and g2 = gcd(nb, da), then 1569 | # 1570 | # na*nb na*nb (na//g1)*(nb//g2) 1571 | # a*b == ----- == ----- == ----------------- 1572 | # da*db db*da (db//g1)*(da//g2) 1573 | # 1574 | # Note, that after divisions we're multiplying smaller integers. 1575 | # 1576 | # Also, the resulting fraction is normalized, because each of 1577 | # two factors in the numerator is coprime to each of the two factors 1578 | # in the denominator. 1579 | # 1580 | # Indeed, pick (na//g1). It's coprime with (da//g2), because input 1581 | # fractions are normalized. It's also coprime with (db//g1), because 1582 | # common factors are removed by g1 == gcd(na, db). 1583 | # 1584 | # As for addition/subtraction, we should special-case g1 == 1 1585 | # and g2 == 1 for same reason. That happens also for multiplying 1586 | # rationals, obtained from floats. 1587 | 1588 | cdef _add(na, da, nb, db): 1589 | """a + b""" 1590 | # return Fraction(na * db + nb * da, da * db) 1591 | g = _gcd(da, db) 1592 | if g == 1: 1593 | return _fraction_from_coprime_ints(na * db + da * nb, da * db) 1594 | s = da // g 1595 | t = na * (db // g) + nb * s 1596 | g2 = _gcd(t, g) 1597 | if g2 == 1: 1598 | return _fraction_from_coprime_ints(t, s * db) 1599 | return _fraction_from_coprime_ints(t // g2, s * (db // g2)) 1600 | 1601 | cdef _sub(na, da, nb, db): 1602 | """a - b""" 1603 | # return Fraction(na * db - nb * da, da * db) 1604 | g = _gcd(da, db) 1605 | if g == 1: 1606 | return _fraction_from_coprime_ints(na * db - da * nb, da * db) 1607 | s = da // g 1608 | t = na * (db // g) - nb * s 1609 | g2 = _gcd(t, g) 1610 | if g2 == 1: 1611 | return _fraction_from_coprime_ints(t, s * db) 1612 | return _fraction_from_coprime_ints(t // g2, s * (db // g2)) 1613 | 1614 | cdef _mul(na, da, nb, db): 1615 | """a * b""" 1616 | # return Fraction(na * nb, da * db) 1617 | g1 = _gcd(na, db) 1618 | if g1 > 1: 1619 | na //= g1 1620 | db //= g1 1621 | g2 = _gcd(nb, da) 1622 | if g2 > 1: 1623 | nb //= g2 1624 | da //= g2 1625 | return _fraction_from_coprime_ints(na * nb, db * da) 1626 | 1627 | cdef _div(na, da, nb, db): 1628 | """a / b""" 1629 | # return Fraction(na * db, da * nb) 1630 | # Same as _mul(), with inversed b. 1631 | if nb == 0: 1632 | raise ZeroDivisionError(f'Fraction({db}, 0)') 1633 | g1 = _gcd(na, nb) 1634 | if g1 > 1: 1635 | na //= g1 1636 | nb //= g1 1637 | g2 = _gcd(db, da) 1638 | if g2 > 1: 1639 | da //= g2 1640 | db //= g2 1641 | n, d = na * db, nb * da 1642 | if d < 0: 1643 | n, d = -n, -d 1644 | return _fraction_from_coprime_ints(n, d) 1645 | 1646 | cdef _floordiv(an, ad, bn, bd): 1647 | """a // b -> int""" 1648 | return (an * bd) // (bn * ad) 1649 | 1650 | cdef _divmod(an, ad, bn, bd): 1651 | div, n_mod = divmod(an * bd, ad * bn) 1652 | return div, Fraction(n_mod, ad * bd) 1653 | 1654 | cdef _mod(an, ad, bn, bd): 1655 | return Fraction((an * bd) % (bn * ad), ad * bd) 1656 | 1657 | 1658 | """ 1659 | In general, we want to implement the arithmetic operations so 1660 | that mixed-mode operations either call an implementation whose 1661 | author knew about the types of both arguments, or convert both 1662 | to the nearest built in type and do the operation there. In 1663 | Fraction, that means that we define __add__ and __radd__ as: 1664 | 1665 | def __add__(self, other): 1666 | # Both types have numerators/denominator attributes, 1667 | # so do the operation directly 1668 | if isinstance(other, (int, Fraction)): 1669 | return Fraction(self.numerator * other.denominator + 1670 | other.numerator * self.denominator, 1671 | self.denominator * other.denominator) 1672 | # float and complex don't have those operations, but we 1673 | # know about those types, so special case them. 1674 | elif isinstance(other, float): 1675 | return float(self) + other 1676 | elif isinstance(other, complex): 1677 | return complex(self) + other 1678 | # Let the other type take over. 1679 | return NotImplemented 1680 | 1681 | def __radd__(self, other): 1682 | # radd handles more types than add because there's 1683 | # nothing left to fall back to. 1684 | if isinstance(other, Rational): 1685 | return Fraction(self.numerator * other.denominator + 1686 | other.numerator * self.denominator, 1687 | self.denominator * other.denominator) 1688 | elif isinstance(other, Real): 1689 | return float(other) + float(self) 1690 | elif isinstance(other, Complex): 1691 | return complex(other) + complex(self) 1692 | return NotImplemented 1693 | 1694 | 1695 | There are 5 different cases for a mixed-type addition on 1696 | Fraction. I'll refer to all of the above code that doesn't 1697 | refer to Fraction, float, or complex as "boilerplate". 'r' 1698 | will be an instance of Fraction, which is a subtype of 1699 | Rational (r : Fraction <: Rational), and b : B <: 1700 | Complex. The first three involve 'r + b': 1701 | 1702 | 1. If B <: Fraction, int, float, or complex, we handle 1703 | that specially, and all is well. 1704 | 2. If Fraction falls back to the boilerplate code, and it 1705 | were to return a value from __add__, we'd miss the 1706 | possibility that B defines a more intelligent __radd__, 1707 | so the boilerplate should return NotImplemented from 1708 | __add__. In particular, we don't handle Rational 1709 | here, even though we could get an exact answer, in case 1710 | the other type wants to do something special. 1711 | 3. If B <: Fraction, Python tries B.__radd__ before 1712 | Fraction.__add__. This is ok, because it was 1713 | implemented with knowledge of Fraction, so it can 1714 | handle those instances before delegating to Real or 1715 | Complex. 1716 | 1717 | The next two situations describe 'b + r'. We assume that b 1718 | didn't know about Fraction in its implementation, and that it 1719 | uses similar boilerplate code: 1720 | 1721 | 4. If B <: Rational, then __radd_ converts both to the 1722 | builtin rational type (hey look, that's us) and 1723 | proceeds. 1724 | 5. Otherwise, __radd__ tries to find the nearest common 1725 | base ABC, and fall back to its builtin type. Since this 1726 | class doesn't subclass a concrete type, there's no 1727 | implementation to fall back to, so we need to try as 1728 | hard as possible to return an actual value, or the user 1729 | will get a TypeError. 1730 | """ 1731 | 1732 | cdef: 1733 | _math_op_add = operator.add 1734 | _math_op_sub = operator.sub 1735 | _math_op_mul = operator.mul 1736 | _math_op_div = getattr(operator, 'div', operator.truediv) # Py2/3 1737 | _math_op_truediv = operator.truediv 1738 | _math_op_floordiv = operator.floordiv 1739 | _math_op_mod = operator.mod 1740 | _math_op_divmod = divmod 1741 | 1742 | 1743 | ctypedef object (*math_func)(an, ad, bn, bd) 1744 | 1745 | 1746 | cdef forward(a, b, math_func monomorphic_operator, pyoperator, handle_complex=True): 1747 | an, ad = (a)._numerator, (a)._denominator 1748 | if type(b) is Fraction: 1749 | return monomorphic_operator(an, ad, (b)._numerator, (b)._denominator) 1750 | elif isinstance(b, int): 1751 | return monomorphic_operator(an, ad, b, 1) 1752 | elif isinstance(b, Fraction): 1753 | return monomorphic_operator(an, ad, b.numerator, b.denominator) 1754 | elif isinstance(b, float): 1755 | return pyoperator(_as_float(an, ad), b) 1756 | elif handle_complex and isinstance(b, complex): 1757 | return pyoperator(float(a), b) 1758 | else: 1759 | return NotImplemented 1760 | 1761 | 1762 | cdef reverse(a, b, math_func monomorphic_operator, pyoperator, handle_complex=True): 1763 | bn, bd = (b)._numerator, (b)._denominator 1764 | if isinstance(a, int): 1765 | return monomorphic_operator(a, 1, bn, bd) 1766 | elif isinstance(a, Rational): 1767 | return monomorphic_operator(a.numerator, a.denominator, bn, bd) 1768 | elif isinstance(a, Real): 1769 | return pyoperator(float(a), _as_float(bn, bd)) 1770 | elif handle_complex and isinstance(a, Complex): 1771 | return pyoperator(complex(a), float(b)) 1772 | else: 1773 | return NotImplemented 1774 | 1775 | 1776 | ctypedef char* charptr 1777 | 1778 | ctypedef fused AnyString: 1779 | unicode 1780 | charptr 1781 | 1782 | 1783 | cdef enum ParserState: 1784 | BEGIN_SPACE # '\s'* -> (BEGIN_SIGN, SMALL_NUM, START_DECIMAL_DOT) 1785 | BEGIN_SIGN # [+-] -> (SMALL_NUM, SMALL_DECIMAL_DOT) 1786 | SMALL_NUM # [0-9]+ -> (SMALL_NUM, SMALL_NUM_US, NUM, NUM_SPACE, SMALL_DECIMAL_DOT, EXP_E, DENOM_START) 1787 | SMALL_NUM_US # '_' -> (SMALL_NUM, NUM) 1788 | NUM # [0-9]+ -> (NUM, NUM_US, NUM_SPACE, DECIMAL_DOT, EXP_E, DENOM_START) 1789 | NUM_US # '_' -> (NUM) 1790 | NUM_SPACE # '\s'+ -> (DENOM_START) 1791 | 1792 | # 1) floating point syntax 1793 | START_DECIMAL_DOT # '.' -> (SMALL_DECIMAL) 1794 | SMALL_DECIMAL_DOT # '.' -> (SMALL_DECIMAL, EXP_E, SMALL_END_SPACE) 1795 | DECIMAL_DOT # '.' -> (DECIMAL, EXP_E, END_SPACE) 1796 | SMALL_DECIMAL # [0-9]+ -> (SMALL_DECIMAL, SMALL_DECIMAL_US, DECIMAL, EXP_E, SMALL_END_SPACE) 1797 | SMALL_DECIMAL_US # '_' -> (SMALL_DECIMAL, DECIMAL) 1798 | DECIMAL # [0-9]+ -> (DECIMAL, DECIMAL_US, EXP_E, END_SPACE) 1799 | DECIMAL_US # '_' -> (DECIMAL) 1800 | EXP_E # [eE] -> (EXP_SIGN, EXP) 1801 | EXP_SIGN # [+-] -> (EXP) 1802 | EXP # [0-9]+ -> (EXP_US, END_SPACE) 1803 | EXP_US # '_' -> (EXP) 1804 | END_SPACE # '\s'+ 1805 | SMALL_END_SPACE # '\s'+ 1806 | 1807 | # 2) "NOM / DENOM" syntax 1808 | DENOM_START # '/' -> (DENOM_SIGN, SMALL_DENOM) 1809 | DENOM_SIGN # [+-] -> (SMALL_DENOM) 1810 | SMALL_DENOM # [0-9]+ -> (SMALL_DENOM, SMALL_DENOM_US, DENOM, DENOM_SPACE) 1811 | SMALL_DENOM_US # '_' -> (SMALL_DENOM) 1812 | DENOM # [0-9]+ -> (DENOM, DENOM_US, DENOM_SPACE) 1813 | DENOM_US # '_' -> (DENOM) 1814 | DENOM_SPACE # '\s'+ 1815 | 1816 | 1817 | cdef _raise_invalid_input(s): 1818 | s = repr(s) 1819 | if s[:2] in ('b"', "b'"): 1820 | s = s[1:] 1821 | raise ValueError(f'Invalid literal for Fraction: {s}') from None 1822 | 1823 | 1824 | cdef _raise_parse_overflow(s): 1825 | s = repr(s) 1826 | if s[0] == 'b': 1827 | s = s[1:] 1828 | raise ValueError(f"Exponent too large for Fraction: {s}") from None 1829 | 1830 | 1831 | cdef extern from *: 1832 | """ 1833 | static CYTHON_INLINE int __QUICKTIONS_unpack_ustring( 1834 | PyObject* string, Py_ssize_t *length, void** data, int *kind) { 1835 | if (PyUnicode_READY(string) < 0) return -1; 1836 | *kind = PyUnicode_KIND(string); 1837 | *length = PyUnicode_GET_LENGTH(string); 1838 | *data = PyUnicode_DATA(string); 1839 | return 0; 1840 | } 1841 | #define __QUICKTIONS_char_at(data, kind, index) \ 1842 | (((kind == 1) ? (Py_UCS4) ((char*) data)[index] : (Py_UCS4) PyUnicode_READ(kind, data, index))) 1843 | """ 1844 | int _unpack_ustring "__QUICKTIONS_unpack_ustring" ( 1845 | object string, Py_ssize_t *length, void **data, int *kind) except -1 1846 | Py_UCS4 _char_at "__QUICKTIONS_char_at" (void *data, int kind, Py_ssize_t index) 1847 | Py_UCS4 PyUnicode_READ(int kind, void *data, Py_ssize_t index) 1848 | 1849 | 1850 | cdef inline int _parse_digit(char** c_digits, Py_UCS4 c, int allow_unicode): 1851 | cdef unsigned int unum 1852 | cdef int num 1853 | unum = ( c) - '0' # Relies on integer underflow for dots etc. 1854 | if unum > 9: 1855 | if not allow_unicode: 1856 | return -1 1857 | num = Py_UNICODE_TODECIMAL(c) 1858 | if num == -1: 1859 | return -1 1860 | unum = num 1861 | c = (num + c'0') 1862 | if c_digits: 1863 | c_digits[0][0] = c 1864 | c_digits[0] += 1 1865 | return unum 1866 | 1867 | 1868 | cdef inline object _parse_pylong(char* c_digits_start, char** c_digits_end): 1869 | c_digits_end[0][0] = 0 1870 | py_number = PyLong_FromString(c_digits_start, NULL, 10) 1871 | c_digits_end[0] = c_digits_start # reset 1872 | return py_number 1873 | 1874 | 1875 | @cython.cdivision(True) 1876 | cdef tuple _parse_fraction(AnyString s, Py_ssize_t s_len, orig_str): 1877 | """ 1878 | Parse a string into a number tuple: (numerator, denominator, is_normalised) 1879 | """ 1880 | cdef Py_ssize_t pos, decimal_len = 0 1881 | cdef Py_UCS4 c 1882 | cdef ParserState state = BEGIN_SPACE 1883 | 1884 | cdef bint is_neg = False, exp_is_neg = False 1885 | cdef int digit 1886 | cdef unsigned int udigit 1887 | cdef long long inum = 0, idecimal = 0, idenom = 0, iexp = 0 1888 | cdef ullong igcd 1889 | cdef object num = None, decimal, denom 1890 | # 2^n > 10^(n * 5/17) 1891 | cdef Py_ssize_t max_decimal_len = (sizeof(inum) * 8) * 5 // 17 1892 | 1893 | # Incremental Unicode iteration isn't in Cython yet. 1894 | cdef int allow_unicode = AnyString is unicode 1895 | cdef int s_kind = 1 1896 | cdef void* s_data = NULL 1897 | cdef char* cdata = NULL 1898 | 1899 | if AnyString is unicode: 1900 | _unpack_ustring(s, &s_len, &s_data, &s_kind) 1901 | if s_kind == 1: 1902 | return _parse_fraction( s_data, s_len, orig_str) 1903 | cdata = s_data 1904 | cdata += 0 # mark used 1905 | else: 1906 | cdata = s 1907 | 1908 | # We collect the digits in inum / idenum as long as the value fits their integer size 1909 | # and additionally in a char* buffer in case it grows too large. 1910 | cdef bytes digits = b'\0' * s_len 1911 | cdef char* c_digits_start = digits 1912 | cdef char* c_digits = c_digits_start 1913 | 1914 | pos = 0 1915 | while pos < s_len: 1916 | c = _char_at(s_data, s_kind, pos) if AnyString is unicode else cdata[pos] 1917 | pos += 1 1918 | digit = _parse_digit(&c_digits, c, allow_unicode) 1919 | if digit == -1: 1920 | if c == u'/': 1921 | if state == SMALL_NUM: 1922 | num = inum 1923 | elif state in (NUM, NUM_SPACE): 1924 | num = _parse_pylong(c_digits_start, &c_digits) 1925 | else: 1926 | _raise_invalid_input(orig_str) 1927 | state = DENOM_START 1928 | break 1929 | elif c == u'.': 1930 | if state in (BEGIN_SPACE, BEGIN_SIGN): 1931 | state = START_DECIMAL_DOT 1932 | elif state == SMALL_NUM: 1933 | state = SMALL_DECIMAL_DOT 1934 | elif state == NUM: 1935 | state = DECIMAL_DOT 1936 | else: 1937 | _raise_invalid_input(orig_str) 1938 | break 1939 | elif c in u'eE': 1940 | if state == SMALL_NUM: 1941 | num = inum 1942 | elif state == NUM: 1943 | num = _parse_pylong(c_digits_start, &c_digits) 1944 | else: 1945 | _raise_invalid_input(orig_str) 1946 | state = EXP_E 1947 | break 1948 | elif c in u'-+': 1949 | if state == BEGIN_SPACE: 1950 | is_neg = c == u'-' 1951 | state = BEGIN_SIGN 1952 | else: 1953 | _raise_invalid_input(orig_str) 1954 | continue 1955 | elif c == u'_': 1956 | if state == SMALL_NUM: 1957 | state = SMALL_NUM_US 1958 | elif state == NUM: 1959 | state = NUM_US 1960 | else: 1961 | _raise_invalid_input(orig_str) 1962 | continue 1963 | else: 1964 | if c.isspace(): 1965 | while pos < s_len: 1966 | c = _char_at(s_data, s_kind, pos) if AnyString is unicode else cdata[pos] 1967 | if not c.isspace(): 1968 | break 1969 | pos += 1 1970 | 1971 | if state in (BEGIN_SPACE, NUM_SPACE): 1972 | continue 1973 | elif state == SMALL_NUM: 1974 | num = inum 1975 | state = NUM_SPACE 1976 | elif state == NUM: 1977 | num = _parse_pylong(c_digits_start, &c_digits) 1978 | state = NUM_SPACE 1979 | else: 1980 | _raise_invalid_input(orig_str) 1981 | continue 1982 | 1983 | _raise_invalid_input(orig_str) 1984 | continue 1985 | 1986 | # normal digit found 1987 | if state in (BEGIN_SPACE, BEGIN_SIGN, SMALL_NUM, SMALL_NUM_US): 1988 | inum = inum * 10 + digit 1989 | state = SMALL_NUM 1990 | 1991 | # fast-path for consecutive digits 1992 | while pos < s_len and inum <= MAX_SMALL_NUMBER: 1993 | c = _char_at(s_data, s_kind, pos) if AnyString is unicode else cdata[pos] 1994 | digit = _parse_digit(&c_digits, c, allow_unicode) 1995 | if digit == -1: 1996 | break 1997 | inum = inum * 10 + digit 1998 | pos += 1 1999 | 2000 | if inum > MAX_SMALL_NUMBER: 2001 | state = NUM 2002 | elif state == NUM_US: 2003 | state = NUM 2004 | 2005 | # We might have switched to NUM above, so continue right here in that case. 2006 | if state == SMALL_NUM: 2007 | pass # handled above 2008 | elif state == NUM: 2009 | # fast-path for consecutive digits 2010 | while pos < s_len: 2011 | c = _char_at(s_data, s_kind, pos) if AnyString is unicode else cdata[pos] 2012 | digit = _parse_digit(&c_digits, c, allow_unicode) 2013 | if digit == -1: 2014 | break 2015 | pos += 1 2016 | else: 2017 | _raise_invalid_input(orig_str) 2018 | 2019 | if state == DENOM_START: 2020 | # NUM '/' | SMALL_NUM '/' 2021 | while pos < s_len: 2022 | c = _char_at(s_data, s_kind, pos) if AnyString is unicode else cdata[pos] 2023 | if not c.isspace(): 2024 | break 2025 | pos += 1 2026 | 2027 | # Start fresh digits collection. 2028 | c_digits = c_digits_start 2029 | while pos < s_len: 2030 | c = _char_at(s_data, s_kind, pos) if AnyString is unicode else cdata[pos] 2031 | pos += 1 2032 | digit = _parse_digit(&c_digits, c, allow_unicode) 2033 | if digit == -1: 2034 | if c in u'-+': 2035 | if state == DENOM_START: 2036 | is_neg ^= (c == u'-') 2037 | state = DENOM_SIGN 2038 | else: 2039 | _raise_invalid_input(orig_str) 2040 | continue 2041 | elif c == u'_': 2042 | if state == SMALL_DENOM: 2043 | state = SMALL_DENOM_US 2044 | elif state == DENOM: 2045 | state = DENOM_US 2046 | else: 2047 | _raise_invalid_input(orig_str) 2048 | continue 2049 | else: 2050 | if c.isspace(): 2051 | while pos < s_len: 2052 | c = _char_at(s_data, s_kind, pos) if AnyString is unicode else cdata[pos] 2053 | if not c.isspace(): 2054 | break 2055 | pos += 1 2056 | 2057 | if state in (DENOM_START, DENOM_SPACE): 2058 | pass 2059 | elif state == SMALL_DENOM: 2060 | denom = idenom 2061 | elif state == DENOM: 2062 | denom = _parse_pylong(c_digits_start, &c_digits) 2063 | else: 2064 | _raise_invalid_input(orig_str) 2065 | state = DENOM_SPACE 2066 | continue 2067 | 2068 | _raise_invalid_input(orig_str) 2069 | continue 2070 | 2071 | # normal digit found 2072 | if state in (DENOM_START, DENOM_SIGN, SMALL_DENOM, SMALL_DENOM_US): 2073 | idenom = idenom * 10 + digit 2074 | state = SMALL_DENOM 2075 | 2076 | # fast-path for consecutive digits 2077 | while pos < s_len and idenom <= MAX_SMALL_NUMBER: 2078 | c = _char_at(s_data, s_kind, pos) if AnyString is unicode else cdata[pos] 2079 | digit = _parse_digit(&c_digits, c, allow_unicode) 2080 | if digit == -1: 2081 | break 2082 | idenom = idenom * 10 + digit 2083 | pos += 1 2084 | 2085 | if idenom > MAX_SMALL_NUMBER: 2086 | state = DENOM 2087 | elif state == DENOM_US: 2088 | state = DENOM 2089 | 2090 | # We might have switched to DENOM above, so continue right here in that case. 2091 | if state == SMALL_DENOM: 2092 | pass # handled above 2093 | elif state == DENOM: 2094 | # fast-path for consecutive digits 2095 | while pos < s_len: 2096 | c = _char_at(s_data, s_kind, pos) if AnyString is unicode else cdata[pos] 2097 | digit = _parse_digit(&c_digits, c, allow_unicode) 2098 | if digit == -1: 2099 | break 2100 | pos += 1 2101 | else: 2102 | _raise_invalid_input(orig_str) 2103 | 2104 | elif state in (SMALL_DECIMAL_DOT, START_DECIMAL_DOT): 2105 | # SMALL_NUM '.' | '.' 2106 | while pos < s_len: 2107 | c = _char_at(s_data, s_kind, pos) if AnyString is unicode else cdata[pos] 2108 | pos += 1 2109 | digit = _parse_digit(&c_digits, c, allow_unicode) 2110 | if digit == -1: 2111 | if c == u'_': 2112 | if state == SMALL_DECIMAL: 2113 | state = SMALL_DECIMAL_US 2114 | else: 2115 | _raise_invalid_input(orig_str) 2116 | continue 2117 | elif c in u'eE': 2118 | if state in (SMALL_DECIMAL_DOT, SMALL_DECIMAL): 2119 | num = inum 2120 | else: 2121 | _raise_invalid_input(orig_str) 2122 | state = EXP_E 2123 | break 2124 | else: 2125 | if c.isspace(): 2126 | while pos < s_len: 2127 | c = _char_at(s_data, s_kind, pos) if AnyString is unicode else cdata[pos] 2128 | if not c.isspace(): 2129 | break 2130 | pos += 1 2131 | 2132 | if state in (SMALL_DECIMAL, SMALL_DECIMAL_DOT): 2133 | num = inum 2134 | state = SMALL_END_SPACE 2135 | else: 2136 | _raise_invalid_input(orig_str) 2137 | continue 2138 | 2139 | _raise_invalid_input(orig_str) 2140 | continue 2141 | 2142 | # normal digit found 2143 | if state in (START_DECIMAL_DOT, SMALL_DECIMAL_DOT, SMALL_DECIMAL, SMALL_DECIMAL_US): 2144 | inum = inum * 10 + digit 2145 | decimal_len += 1 2146 | state = SMALL_DECIMAL 2147 | 2148 | # fast-path for consecutive digits 2149 | while pos < s_len and inum <= MAX_SMALL_NUMBER and decimal_len < max_decimal_len: 2150 | c = _char_at(s_data, s_kind, pos) if AnyString is unicode else cdata[pos] 2151 | digit = _parse_digit(&c_digits, c, allow_unicode) 2152 | if digit == -1: 2153 | break 2154 | inum = inum * 10 + digit 2155 | decimal_len += 1 2156 | pos += 1 2157 | 2158 | if inum > MAX_SMALL_NUMBER or decimal_len >= max_decimal_len: 2159 | state = DECIMAL 2160 | break 2161 | else: 2162 | _raise_invalid_input(orig_str) 2163 | 2164 | if state in (DECIMAL_DOT, DECIMAL): 2165 | # NUM '.' | SMALL_DECIMAL->DECIMAL 2166 | while pos < s_len: 2167 | c = _char_at(s_data, s_kind, pos) if AnyString is unicode else cdata[pos] 2168 | pos += 1 2169 | digit = _parse_digit(&c_digits, c, allow_unicode) 2170 | if digit == -1: 2171 | if c == u'_': 2172 | if state == DECIMAL: 2173 | state = DECIMAL_US 2174 | else: 2175 | _raise_invalid_input(orig_str) 2176 | continue 2177 | elif c in u'eE': 2178 | if state in (DECIMAL_DOT, DECIMAL): 2179 | num = _parse_pylong(c_digits_start, &c_digits) 2180 | else: 2181 | _raise_invalid_input(orig_str) 2182 | state = EXP_E 2183 | break 2184 | else: 2185 | if c.isspace(): 2186 | if state in (DECIMAL, DECIMAL_DOT): 2187 | state = END_SPACE 2188 | else: 2189 | _raise_invalid_input(orig_str) 2190 | break 2191 | 2192 | _raise_invalid_input(orig_str) 2193 | continue 2194 | 2195 | # normal digit found 2196 | if state in (DECIMAL_DOT, DECIMAL, DECIMAL_US): 2197 | decimal_len += 1 2198 | state = DECIMAL 2199 | 2200 | # fast-path for consecutive digits 2201 | while pos < s_len: 2202 | c = _char_at(s_data, s_kind, pos) if AnyString is unicode else cdata[pos] 2203 | digit = _parse_digit(&c_digits, c, allow_unicode) 2204 | if digit == -1: 2205 | break 2206 | decimal_len += 1 2207 | pos += 1 2208 | else: 2209 | _raise_invalid_input(orig_str) 2210 | 2211 | if state == EXP_E: 2212 | # (SMALL_) NUM ['.' DECIMAL] 'E' 2213 | while pos < s_len: 2214 | c = _char_at(s_data, s_kind, pos) if AnyString is unicode else cdata[pos] 2215 | pos += 1 2216 | digit = _parse_digit(NULL, c, allow_unicode) 2217 | if digit == -1: 2218 | if c in u'-+': 2219 | if state == EXP_E: 2220 | exp_is_neg = c == u'-' 2221 | state = EXP_SIGN 2222 | else: 2223 | _raise_invalid_input(orig_str) 2224 | continue 2225 | elif c == u'_': 2226 | if state == EXP: 2227 | state = EXP_US 2228 | else: 2229 | _raise_invalid_input(orig_str) 2230 | continue 2231 | else: 2232 | if c.isspace(): 2233 | if state == EXP: 2234 | state = END_SPACE 2235 | else: 2236 | _raise_invalid_input(orig_str) 2237 | break 2238 | 2239 | _raise_invalid_input(orig_str) 2240 | continue 2241 | 2242 | # normal digit found 2243 | if state in (EXP_E, EXP_SIGN, EXP, EXP_US): 2244 | iexp = iexp * 10 + digit 2245 | state = EXP 2246 | 2247 | # fast-path for consecutive digits 2248 | while pos < s_len and iexp <= MAX_SMALL_NUMBER: 2249 | c = _char_at(s_data, s_kind, pos) if AnyString is unicode else cdata[pos] 2250 | digit = _parse_digit(NULL, c, allow_unicode) 2251 | if digit == -1: 2252 | break 2253 | iexp = iexp * 10 + digit 2254 | pos += 1 2255 | 2256 | if iexp > MAX_SMALL_NUMBER: 2257 | _raise_parse_overflow(orig_str) 2258 | else: 2259 | _raise_invalid_input(orig_str) 2260 | 2261 | if state in (END_SPACE, SMALL_END_SPACE, DENOM_SPACE): 2262 | while pos < s_len: 2263 | c = _char_at(s_data, s_kind, pos) if AnyString is unicode else cdata[pos] 2264 | if not c.isspace(): 2265 | break 2266 | pos += 1 2267 | 2268 | if pos < s_len : 2269 | _raise_invalid_input(orig_str) 2270 | 2271 | cdef bint is_normalised = False 2272 | if state in (SMALL_NUM, SMALL_DECIMAL, SMALL_DECIMAL_DOT, SMALL_END_SPACE): 2273 | # Special case for 'small' numbers: normalise directly in C space. 2274 | if inum and decimal_len: 2275 | # Only need to normalise if the numerator contains factors of a power of 10 (2 or 5). 2276 | if inum & 1 == 0 or inum % 5 == 0: 2277 | idenom = _c_pow10(decimal_len) 2278 | igcd = _c_gcd(inum, idenom) 2279 | if igcd > 1: 2280 | inum //= igcd 2281 | denom = idenom // igcd 2282 | else: 2283 | denom = pow10(decimal_len) 2284 | else: 2285 | denom = pow10(decimal_len) 2286 | else: 2287 | denom = 1 2288 | if is_neg: 2289 | inum = -inum 2290 | return inum, denom, True 2291 | 2292 | elif state == SMALL_DENOM: 2293 | denom = idenom 2294 | elif state in (NUM, DECIMAL, DECIMAL_DOT): 2295 | is_normalised = True # will be repaired below for iexp < 0 2296 | denom = 1 2297 | num = _parse_pylong(c_digits_start, &c_digits) 2298 | elif state == DENOM: 2299 | denom = _parse_pylong(c_digits_start, &c_digits) 2300 | elif state in (NUM_SPACE, EXP, END_SPACE): 2301 | is_normalised = True 2302 | denom = 1 2303 | elif state == DENOM_SPACE: 2304 | pass 2305 | else: 2306 | _raise_invalid_input(orig_str) 2307 | 2308 | if decimal_len > MAX_SMALL_NUMBER: 2309 | _raise_parse_overflow(orig_str) 2310 | if exp_is_neg: 2311 | iexp = -iexp 2312 | iexp -= decimal_len 2313 | 2314 | if iexp > 0: 2315 | num *= pow10(iexp) 2316 | elif iexp < 0: 2317 | # Only need to normalise if the numerator contains factors of a power of 10 (2 or 5). 2318 | is_normalised = num & 1 != 0 and num % 5 != 0 2319 | denom = pow10(-iexp) 2320 | 2321 | if is_neg: 2322 | num = -num 2323 | 2324 | return num, denom, is_normalised 2325 | --------------------------------------------------------------------------------