├── .coveragerc ├── .gitignore ├── AUTHORS.rst ├── CONTRIBUTING.md ├── HISTORY.md ├── LICENSE ├── README.md ├── docs └── pairing_functions.md ├── pairing_functions ├── __init__.py ├── __version__.py ├── cantor.py └── szudzik.py ├── requirements.txt ├── setup.py └── tests ├── __init__.py ├── test_cantor.py └── test_szudzik.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = /usr/* 3 | *etc/* 4 | *site-packages* 5 | *__init__* 6 | *tests* 7 | setup.py 8 | *__version__.py 9 | 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | test_coverage 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # pyenv 79 | .python-version 80 | 81 | # celery beat schedule file 82 | celerybeat-schedule 83 | 84 | # SageMath parsed files 85 | *.sage.py 86 | 87 | # Environments 88 | .env 89 | .venv 90 | env/ 91 | venv/ 92 | ENV/ 93 | env.bak/ 94 | venv.bak/ 95 | 96 | # Spyder project settings 97 | .spyderproject 98 | .spyproject 99 | 100 | # Rope project settings 101 | .ropeproject 102 | 103 | # Jetbrains settings 104 | .idea 105 | 106 | # mkdocs documentation 107 | /site 108 | 109 | # mypy 110 | .mypy_cache/ 111 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | **pairing-functions** is written and maintained by the following contributors: 2 | 3 | - Antonopoulos Ilias 4 | - Koutsioubi Kelly 5 | - Pechlivanis Konstantinos 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to this project 2 | Contributions to this project are welcome. Please take a moment to read this document, in order to make the contribution process easier for everyone involved. 3 | 4 | ## Bug Reports 5 | - Before opening a bug report, please use the Github issue search to check if the issue has already been reported. 6 | - When you are creating a bug report, please include as many details as possible: expected vs. observed behavior, steps to reproduce, a description of your environment. 7 | 8 | ## Feature Requests 9 | Feature requests are welcome. But do take a moment to find out whether your idea fits with the scope and aims of the project. Please provide as much detail and context as possible. 10 | 11 | ## Pull Requests 12 | Pull requests are very welcome — be they bug fixes, improvements or new features. Before embarking on a significant pull request, please first discuss the change you wish to make with the owners of this repository, by creating an issue. 13 | 14 | If you are not familiar with the Github contributing process, you can take a look at [this](https://www.gun.io/blog/how-to-github-fork-branch-and-pull-request) guide. 15 | 16 | Please make sure your PRs: 17 | - Pass all tests (and — in case of new features — add appropriate tests). 18 | - Adhere to the coding conventions used throughout the project. 19 | - Include as few commits as possible, with clear commit messages, avoiding mixing code changes with whitespace cleanup. 20 | - Are limited to a single feature/issue. 21 | - Have a clear title and description. 22 | - Have you included in [AUTHORS](AUTHORS.rst). 23 | 24 | :point_up: By submitting work for inclusion to this project, you agree to allow the project owner to license your work under the same license as that used by the project. 25 | 26 | Thank you! :heart: 27 | 28 | **Convert Group Developers** -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | Release History 2 | =============== 3 | 4 | 0.2.1 (2020-10-07) 5 | ------------------ 6 | 7 | - Minor documentation issues 8 | 9 | 0.2.0 (2020-06-03) 10 | ------------------ 11 | 12 | - Cantor pairing function 13 | 14 | 0.1.0 (2020-04-10) 15 | ------------------ 16 | 17 | - Szudzik pairing function 18 | 19 | 0.1.0 (2019-06-06) 20 | ------------------ 21 | 22 | - First commit! 23 | 24 | 0.0.1 (2017-06-30) 25 | ------------------ 26 | 27 | - Conception 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2019 Convert Group https://www.convertgroup.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pairing-functions 2 | 3 | A pairing function is a function that *reversibly* maps two non-negative integers onto a single non-negative integer. 4 | 5 | This package currently supports the following pairing functions: 6 | 7 | - **Szudzik pairing function** 8 | - **Cantor pairing function** 9 | 10 | 11 | Install 12 | ------- 13 | 14 | Simply: 15 | ```shell script 16 | $ pip install pairing-functions 17 | ``` 18 | 19 | Usage 20 | ----- 21 | ```python 22 | from pairing_functions import cantor, szudzik 23 | 24 | szudzik.pair(3, 4) 25 | // 19 26 | 27 | szudzik.unpair(19) 28 | // (3, 4) 29 | 30 | cantor.pair(3, 4) 31 | // 32 32 | 33 | cantor.unpair(32) 34 | // (3, 4) 35 | ``` 36 | 37 | You can also work with more than 2 integers: 38 | 39 | ```python 40 | from pairing_functions import cantor, szudzik 41 | 42 | szudzik.pair(1, 2, 3, 4) 43 | // 1126 44 | 45 | cantor.pair(1, 2, 3, 4) 46 | // 2705 47 | 48 | # by default, unpairing will result in two integers 49 | 50 | szudzik.unpair(1126) 51 | // (33, 4) 52 | 53 | cantor.unpair(2705) 54 | // (69, 4) 55 | 56 | # but going back to the initial integers is also possible 57 | # just specify how many integers you expect 58 | 59 | szudzik.unpair(1126, n=4) 60 | // (1, 2, 3, 4) 61 | 62 | cantor.unpair(2705, n=4) 63 | // (1, 2, 3, 4) 64 | ``` 65 | 66 | Documentation 67 | ------------- 68 | You can find more about pairing functions in the [docs](docs/pairing_functions.md). 69 | 70 | How to contribute 71 | ----------------- 72 | If you wish to contribute, you can start from [here](CONTRIBUTING.md) ! 73 | 74 | Test 75 | ---- 76 | You can run the available tests with [pytest](https://docs.pytest.org/en/latest/) - code coverage metrics are also available via [pytest-cov](https://github.com/pytest-dev/pytest-cov). -------------------------------------------------------------------------------- /docs/pairing_functions.md: -------------------------------------------------------------------------------- 1 | **What is a pairing function ?** 2 | 3 | A pairing function reversibly maps two non-negative integers onto a single non-negative integer. 4 | 5 | Two informative resources around pairing functions can be found [here](https://en.wikipedia.org/wiki/Pairing_function) and [here](http://mathworld.wolfram.com/PairingFunction.html). 6 | 7 | **Szudzik pairing function** 8 | 9 | Created by Matthew Szudzik (Wolfram Research, Inc) - [this](http://szudzik.com/ElegantPairing.pdf) is a short presentation. 10 | 11 | **Cantor pairing function** 12 | 13 | Created by Georg Cantor - [this](https://en.wikipedia.org/wiki/Pairing_function#Cantor_pairing_function) is a short presentation. 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /pairing_functions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Convert-Group/pairing-functions/a95ba13295c7c684e445de42a8602418772a407d/pairing_functions/__init__.py -------------------------------------------------------------------------------- /pairing_functions/__version__.py: -------------------------------------------------------------------------------- 1 | __title__ = 'pairing-functions' 2 | __description__ = 'A collection of pairing functions' 3 | __url__ = 'https://github.com/ConvertGroupLabs/pairing-functions' 4 | __version__ = '0.2.1' 5 | __author__ = 'Convert Group Labs' 6 | __author_email__ = 'tools@convertgroup.com' 7 | __license__ = 'MIT License' 8 | __copyright__ = 'Copyright 2020 Convert Group' 9 | -------------------------------------------------------------------------------- /pairing_functions/cantor.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from collections import deque 4 | from math import pow, floor, sqrt 5 | 6 | 7 | def pair(*numbers: int) -> int: 8 | """ 9 | Maps a pair of non-negative integers to a uniquely associated single non-negative integer. 10 | Pairing also generalizes for `n` non-negative integers, by recursively mapping the first pair. 11 | For example, to map the following tuple: 12 | (n_1, n_2, n_3) 13 | the n_1, n_2 pair is mapped accordingly to a number n_p, 14 | and then the n_p, n3 pair is mapped to produce the final association. 15 | """ 16 | if len(numbers) < 2: 17 | raise ValueError('Cantor pairing function needs at least 2 numbers as input') 18 | 19 | elif any((n < 0) or (not isinstance(n, int)) for n in numbers): 20 | raise ValueError('Cantor pairing function maps only non-negative integers') 21 | 22 | numbers = deque(numbers) 23 | 24 | # fetch the first two numbers 25 | n1 = numbers.popleft() 26 | n2 = numbers.popleft() 27 | 28 | mapping = (n1 + n2) * (n1 + n2 + 1) / 2 + n2 29 | 30 | mapping = int(mapping) 31 | 32 | if not numbers: 33 | # recursion concludes 34 | return mapping 35 | else: 36 | numbers.appendleft(mapping) 37 | return pair(*numbers) 38 | 39 | 40 | def unpair(number: int, n: int = 2) -> tuple: 41 | """ 42 | The inverse function outputs the pair associated with a non-negative integer. 43 | Unpairing also generalizes by recursively unpairing a non-negative integer to `n` non-negative integers. 44 | For example, to associate a `number` with three non-negative 45 | integers n_1, n_2, n_3, such that: 46 | 47 | pairing(n_1, n_2, n_3) = `number` 48 | 49 | the `number` will first be unpaired to n_p, n_3, then the n_p will be unpaired to n_1, n_2, 50 | producing the desired n_1, n_2 and n_3. 51 | """ 52 | if (number < 0) or (not isinstance(number, int)): 53 | raise ValueError('Cantor unpairing function requires a non-negative integer') 54 | 55 | w = floor((sqrt(8 * number + 1) - 1) / 2) 56 | t = (pow(w, 2) + w) / 2 57 | 58 | n2 = int(number - t) 59 | n1 = int(w - n2) 60 | 61 | if n > 2: 62 | return unpair(n1, n - 1) + (n2,) 63 | else: 64 | # recursion concludes 65 | return n1, n2 66 | -------------------------------------------------------------------------------- /pairing_functions/szudzik.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from collections import deque 4 | from math import pow, floor, sqrt 5 | 6 | 7 | def pair(*numbers: int) -> int: 8 | """ 9 | Maps a pair of non-negative integers to a uniquely associated single non-negative integer. 10 | Pairing also generalizes for `n` non-negative integers, by recursively mapping the first pair. 11 | For example, to map the following tuple: 12 | (n_1, n_2, n_3) 13 | the n_1, n_2 pair is mapped accordingly to a number n_p, 14 | and then the n_p, n3 pair is mapped to produce the final association. 15 | """ 16 | if len(numbers) < 2: 17 | raise ValueError('Szudzik pairing function needs at least 2 numbers as input') 18 | 19 | elif any((n < 0) or (not isinstance(n, int)) for n in numbers): 20 | raise ValueError('Szudzik pairing function maps only non-negative integers') 21 | 22 | numbers = deque(numbers) 23 | 24 | # fetch the first two numbers 25 | n1 = numbers.popleft() 26 | n2 = numbers.popleft() 27 | 28 | if n1 != max(n1, n2): 29 | mapping = pow(n2, 2) + n1 30 | else: 31 | mapping = pow(n1, 2) + n1 + n2 32 | 33 | mapping = int(mapping) 34 | 35 | if not numbers: 36 | # recursion concludes 37 | return mapping 38 | else: 39 | numbers.appendleft(mapping) 40 | return pair(*numbers) 41 | 42 | 43 | def unpair(number: int, n: int = 2) -> tuple: 44 | """ 45 | The inverse function outputs the pair associated with a non-negative integer. 46 | Unpairing also generalizes by recursively unpairing a non-negative integer to `n` non-negative integers. 47 | For example, to associate a `number` with three non-negative 48 | integers n_1, n_2, n_3, such that: 49 | 50 | pairing(n_1, n_2, n_3) = `number` 51 | 52 | the `number` will first be unpaired to n_p, n_3, then the n_p will be unpaired to n_1, n_2, 53 | producing the desired n_1, n_2 and n_3. 54 | """ 55 | if (number < 0) or (not isinstance(number, int)): 56 | raise ValueError('Szudzik unpairing function requires a non-negative integer') 57 | 58 | if number - pow(floor(sqrt(number)), 2) < floor(sqrt(number)): 59 | 60 | n1 = number - pow(floor(sqrt(number)), 2) 61 | n2 = floor(sqrt(number)) 62 | 63 | else: 64 | n1 = floor(sqrt(number)) 65 | n2 = number - pow(floor(sqrt(number)), 2) - floor(sqrt(number)) 66 | 67 | n1, n2 = int(n1), int(n2) 68 | 69 | if n > 2: 70 | return unpair(n1, n - 1) + (n2,) 71 | else: 72 | # recursion concludes 73 | return n1, n2 74 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pytest==5.4.3 2 | pytest-cov==2.10.0 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from setuptools import setup, find_packages 5 | 6 | # Package meta-data 7 | NAME = 'pairing-functions' 8 | DESCRIPTION = 'A collection of pairing functions' 9 | URL = 'https://github.com/ConvertGroupLabs/pairing-functions' 10 | EMAIL = 'tools@convertgroup.com' 11 | AUTHOR = 'Convert Group Labs' 12 | REQUIRES_PYTHON = '>=3.5.2' 13 | VERSION = (0, 2, 1) 14 | 15 | # What packages are required for this module to be executed? 16 | REQUIRED = [] 17 | 18 | # What packages are required for test suite execution? 19 | TESTS_REQUIRED = [ 20 | 'pytest', 21 | 'pytest-cov' 22 | ] 23 | 24 | # What packages are optional? 25 | EXTRAS = {} 26 | 27 | 28 | with open('README.md', 'r') as f: 29 | long_description = f.read() 30 | 31 | 32 | setup( 33 | name=NAME, 34 | version='.'.join(map(str, VERSION)), 35 | description=DESCRIPTION, 36 | long_description=long_description, 37 | long_description_content_type='text/markdown', 38 | author=AUTHOR, 39 | author_email=EMAIL, 40 | python_requires=REQUIRES_PYTHON, 41 | url=URL, 42 | packages=find_packages(exclude=["tests", "docs"]), 43 | install_requires=REQUIRED, 44 | tests_require=TESTS_REQUIRED, 45 | extras_require=EXTRAS, 46 | include_package_data=True, 47 | license='MIT License', 48 | classifiers=[ 49 | # Trove classifiers 50 | # Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers 51 | 'Natural Language :: English', 52 | 'License :: OSI Approved :: MIT License', 53 | 'Programming Language :: Python', 54 | 'Programming Language :: Python :: 3', 55 | 'Programming Language :: Python :: 3.6', 56 | 'Programming Language :: Python :: Implementation :: CPython', 57 | 'Programming Language :: Python :: Implementation :: PyPy' 58 | ] 59 | ) 60 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Convert-Group/pairing-functions/a95ba13295c7c684e445de42a8602418772a407d/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_cantor.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import pytest 4 | 5 | from pairing_functions.cantor import pair, unpair 6 | 7 | 8 | class TestCantorPairing(object): 9 | 10 | def test_pair(self) -> None: 11 | 12 | assert pair(0, 0) == 0 13 | assert pair(0, 1) == 2 14 | assert pair(1, 0) == 1 15 | assert pair(2, 2) == 12 16 | assert pair(3, 4) == 32 17 | assert pair(47, 32) == 3192 18 | assert pair(92, 23) == 6693 19 | 20 | def test_pair_multiple_numbers(self) -> None: 21 | 22 | assert pair(1, 2, 3) == 69 23 | assert pair(3, 4, 5) == 708 24 | assert pair(1, 2, 3, 4) == 2705 25 | assert pair(1, 2, 3, 4, 5) == 3673410 26 | 27 | def test_pair_exceptions(self) -> None: 28 | 29 | with pytest.raises(ValueError): 30 | assert pair(1) 31 | 32 | with pytest.raises(ValueError): 33 | assert pair(1, -2) 34 | 35 | with pytest.raises(ValueError): 36 | assert pair(1,) 37 | 38 | with pytest.raises(ValueError): 39 | assert pair(1, -2) 40 | 41 | 42 | class TestCantorUnpair(object): 43 | 44 | def test_unpair(self) -> None: 45 | 46 | assert unpair(0) == (0, 0) 47 | assert unpair(1) == (1, 0) 48 | assert unpair(2) == (0, 1) 49 | assert unpair(32) == (3, 4) 50 | assert unpair(1432) == (52, 1) 51 | assert unpair(6693) == (92, 23) 52 | 53 | assert unpair(69) == (8, 3) 54 | assert unpair(69, n=3) == (1, 2, 3) 55 | 56 | assert unpair(708) == (32, 5) 57 | assert unpair(708, n=3) == (3, 4, 5) 58 | 59 | assert unpair(2705) == (69, 4) 60 | assert unpair(2705, n=3) == (8, 3, 4) 61 | assert unpair(2705, n=4) == (1, 2, 3, 4) 62 | 63 | def test_unpair_exceptions(self) -> None: 64 | 65 | with pytest.raises(ValueError): 66 | assert unpair(0.5) 67 | 68 | with pytest.raises(ValueError): 69 | assert unpair(-1) 70 | 71 | 72 | class TestCantor(object): 73 | 74 | def test_inverse_property(self) -> None: 75 | 76 | n1, n2 = unpair(pair(1, 2)) 77 | assert n1 == 1 and n2 == 2 78 | 79 | n1, n2 = unpair(33) 80 | assert pair(n1, n2) == 33 81 | -------------------------------------------------------------------------------- /tests/test_szudzik.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import pytest 4 | 5 | from pairing_functions.szudzik import pair, unpair 6 | 7 | 8 | class TestSzudzikPairing(object): 9 | 10 | def test_pair(self) -> None: 11 | 12 | assert pair(0, 0) == 0 13 | assert pair(0, 1) == 1 14 | assert pair(1, 0) == 2 15 | assert pair(2, 2) == 8 16 | assert pair(3, 4) == 19 17 | assert pair(92, 23) == 8579 18 | 19 | def test_pair_multiple_numbers(self) -> None: 20 | 21 | assert pair(1, 2, 3) == 33 22 | assert pair(3, 4, 5) == 385 23 | assert pair(1, 2, 3, 4) == 1126 24 | assert pair(1, 2, 3, 4, 5) == 1269007 25 | 26 | def test_pair_exceptions(self) -> None: 27 | 28 | with pytest.raises(ValueError): 29 | assert pair(1) 30 | 31 | with pytest.raises(ValueError): 32 | assert pair(1, -2) 33 | 34 | with pytest.raises(ValueError): 35 | assert pair(1,) 36 | 37 | with pytest.raises(ValueError): 38 | assert pair(1, -2) 39 | 40 | 41 | class TestSzudzikUnpair(object): 42 | 43 | def test_unpair(self) -> None: 44 | 45 | assert unpair(0) == (0, 0) 46 | assert unpair(1) == (0, 1) 47 | assert unpair(2) == (1, 0) 48 | assert unpair(19) == (3, 4) 49 | assert unpair(8579) == (92, 23) 50 | 51 | assert unpair(33) == (5, 3) 52 | assert unpair(33, n=3) == (1, 2, 3) 53 | 54 | assert unpair(385) == (19, 5) 55 | assert unpair(385, n=3) == (3, 4, 5) 56 | 57 | assert unpair(1126) == (33, 4) 58 | assert unpair(1126, n=3) == (5, 3, 4) 59 | assert unpair(1126, n=4) == (1, 2, 3, 4) 60 | 61 | def test_unpair_exceptions(self) -> None: 62 | 63 | with pytest.raises(ValueError): 64 | assert unpair(0.5) 65 | 66 | with pytest.raises(ValueError): 67 | assert unpair(-1) 68 | 69 | 70 | class TestSzudzik(object): 71 | 72 | def test_inverse_property(self) -> None: 73 | 74 | n1, n2 = unpair(pair(1, 2)) 75 | assert n1 == 1 and n2 == 2 76 | 77 | n1, n2 = unpair(33) 78 | assert pair(n1, n2) == 33 79 | --------------------------------------------------------------------------------