├── .gitignore ├── LICENSE ├── README.md ├── pyproject.toml ├── rounder ├── __init__.py └── rounder.py └── tests ├── __init__.py ├── conftest.py └── test_rounder_functions.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # Distribution / packaging 7 | .Python 8 | build/ 9 | develop-eggs/ 10 | dist/ 11 | downloads/ 12 | eggs/ 13 | .eggs/ 14 | lib/ 15 | lib64/ 16 | parts/ 17 | sdist/ 18 | var/ 19 | wheels/ 20 | pip-wheel-metadata/ 21 | share/python-wheels/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | MANIFEST 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .nox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | *.py,cover 48 | .hypothesis/ 49 | .pytest_cache/ 50 | 51 | # Environments 52 | .env 53 | .venv 54 | env/ 55 | venv/ 56 | ENV/ 57 | env.bak/ 58 | venv.bak/ 59 | venv_*/ 60 | venv-*/ 61 | 62 | # temporary files 63 | */tmp*.* 64 | 65 | # virtual environments 66 | **/venv* 67 | 68 | # VS Code 69 | **/.vscode/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Ruud van der Ham & Nyggus 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `rounder`: Rounding of numbers in complex Python objects 2 | 3 | `rounder` is a lightweight package for rounding numbers in complex Python objects, such as dictionaries, lists, tuples, and sets, and any complex object that combines any number of such objects in any nested structure; you can also use it for instances of classes whose attributes contain numbers. The code is organized as a Python (Python >= 3.6 is required) package that can be installed from PyPi (`pip install rounder`), but as it is a one-file package, you can simply download its main module ([rounder.py](rounder/rounder.py)) and use it directly in your project. 4 | 5 | The package is useful mainly for presentation purposes, but in some cases, it can be useful in other situations as well. 6 | 7 | `rounder` offers you four functions for rounding objects: 8 | 9 | * `round_object(obj, digits=0, use_copy=False)`, which rounds all numbers in `obj` to `digits` decimal places 10 | * `floor_object(obj, use_copy=False)`, which rounds all numbers in `obj` down to the nearest integer 11 | * `ceil_object(obj, use_copy=False)`, which rounds all numbers in `obj` up to the nearest integer 12 | * `signif_object(obj, digits, use_copy=False)`, which rounds all numbers in `obj` to `digits` significant digits 13 | 14 | In addition, `rounder` comes with a generalized function: 15 | 16 | * `map_obj(func, obj, use_copy=False)`, which runs callable `func`, which takes a number as an argument and returns a number, to all numbers across the object. 17 | 18 | `rounder` also offers a function for rounding numbers to significant digits: 19 | 20 | * `signif(x, digits)`, which rounds `x` (either an int or a float) to `digits` significant digits 21 | 22 | You can use `signif` in a simple way: 23 | 24 | ```python 25 | >>> import rounder as r 26 | >>> r.signif(1.1212, 3) 27 | 1.12 28 | >>> r.signif(12.1239112, 5) 29 | 12.124 30 | >>> r.signif(121212.12, 3) 31 | 121000.0 32 | 33 | ``` 34 | 35 | The package is simple to use, but you have to remember that when you're working with mutable objects, such as dicts or lists, rounding them will affect the original object; no such effect, of course, will occur for immutable types (e.g., tuples and sets). To overcome this effect, simply use `use_copy=True` in the above functions (not in `signif`). If you do so, the function will create a deep copy of the object, work on it, and return it; the original object will not be affected in any way. 36 | 37 | You can use `rounder` functions for rounding floats, but do remember that their behavior is slightly different than that of their `builtin` and `math` counterparts, as the former, unlike the latter, do not throw an exception when a non-number object is used. 38 | 39 | You can round, for example, a list, a tuple, a set (including a frozenset), a double `array.array`, and a dict: 40 | 41 | ```python 42 | >>> r.round_object([1.122, 2.4434], 1) 43 | [1.1, 2.4] 44 | >>> r.ceil_object([1.122, 2.4434]) 45 | [2, 3] 46 | >>> r.floor_object([1.122, 2.4434]) 47 | [1, 2] 48 | >>> r.signif_object([1.1224, 222.4434], 4) 49 | [1.122, 222.4] 50 | 51 | >>> r.round_object((1.122, 2.4434), 1) 52 | (1.1, 2.4) 53 | >>> r.round_object({1.122, 2.4434}, 1) 54 | {1.1, 2.4} 55 | >>> r.round_object({"1": 1.122, "q":2.4434}, 1) 56 | {'1': 1.1, 'q': 2.4} 57 | 58 | >>> import array 59 | >>> arr = array.array("d", (1.122, 2.4434)) 60 | >>> r.round_object(arr, 1) 61 | array('d', [1.1, 2.4]) 62 | 63 | ``` 64 | 65 | As mentioned above, you can use `rounder` functions also for class instances: 66 | 67 | ```python 68 | >>> class ClassWithNumbers: 69 | ... def __init__(self, x, y): 70 | ... self.x = x 71 | ... self.y = y 72 | >>> inst = ClassWithNumbers( 73 | ... x = 20.22045, 74 | ... y={"list": [34.554, 666.777], 75 | ... "tuple": (.111210, 343.3333)} 76 | ... ) 77 | 78 | >>> inst_copy = r.round_object(inst, 1, True) 79 | >>> inst_copy.x 80 | 20.2 81 | >>> inst_copy.y 82 | {'list': [34.6, 666.8], 'tuple': (0.1, 343.3)} 83 | >>> id(inst) != id(inst_copy) 84 | True 85 | 86 | >>> inst.x 87 | 20.22045 88 | >>> inst_no_copy = r.floor_object(inst, False) 89 | >>> id(inst) == id(inst_no_copy) 90 | True 91 | >>> inst.x 92 | 20 93 | 94 | ``` 95 | 96 | You can of course round a particular attribute of the class instance: 97 | 98 | ```python 99 | >>> _ = r.round_object(inst_copy.y, 0, False) 100 | >>> inst_copy.y 101 | {'list': [35.0, 667.0], 'tuple': (0.0, 343.0)} 102 | 103 | ``` 104 | 105 | Note that you do not have to worry about having non-roundable objects in the object fed into the `rounder` functions. Your objects can contain objects of any type; numbers will be rounded while all other objects will remain untouched: 106 | 107 | ```python 108 | >>> r.round_object([1.122, "string", 2.4434, 2.45454545-2j], 1) 109 | [1.1, 'string', 2.4, (2.5-2j)] 110 | 111 | ``` 112 | 113 | In fact, you can round any object, and the function will simply return it if it cannot be rounded: 114 | 115 | ```python 116 | >>> r.round_object("string") 117 | 'string' 118 | >>> r.round_object(lambda x: x**3)(2) 119 | 8 120 | >>> class Example: ... 121 | >>> r.round_object(Example) 122 | 123 | >>> r.round_object(Example()) 124 | <__main__.Example object at 0x...> 125 | 126 | ``` 127 | 128 | But most of all, you can apply rounding for any complex object, of any structure. Imagine you have a structure like this: 129 | 130 | ```python 131 | >>> x = { 132 | ... "items": ["item 1", "item 2", "item 3",], 133 | ... "quantities": {"item 1": 235, "item 2" : 300, "item 3": 17,}, 134 | ... "prices": { 135 | ... "item 1": {"$": 32.22534554, "EURO": 41.783234567}, 136 | ... "item 2": {"$": 42.26625, "EURO": 51.333578}, 137 | ... "item 3": {"$": 2.223043225, "EURO": 2.78098721346} 138 | ... }, 139 | ... "income": { 140 | ... "2009": {"$": 3445342.324364, "EURO": 39080.332546}, 141 | ... "2010": {"$": 6765675.56665554, "EURO": 78980.34564546}, 142 | ... } 143 | ... } 144 | 145 | ``` 146 | 147 | To round all the values in this structure, you would need to build a dedicated function. With `rounder`, this is a piece of cake: 148 | 149 | ```python 150 | >>> rounded_x = r.round_object(x, digits=2, use_copy=True) 151 | 152 | ``` 153 | 154 | And you will get this: 155 | 156 | ```python 157 | >>> from pprint import pprint 158 | >>> pprint(rounded_x) 159 | {'income': {'2009': {'$': 3445342.32, 'EURO': 39080.33}, 160 | '2010': {'$': 6765675.57, 'EURO': 78980.35}}, 161 | 'items': ['item 1', 'item 2', 'item 3'], 162 | 'prices': {'item 1': {'$': 32.23, 'EURO': 41.78}, 163 | 'item 2': {'$': 42.27, 'EURO': 51.33}, 164 | 'item 3': {'$': 2.22, 'EURO': 2.78}}, 165 | 'quantities': {'item 1': 235, 'item 2': 300, 'item 3': 17}} 166 | 167 | ``` 168 | 169 | Note that we used `use_copy=True`, which means that `rounded_x` is a deepcopy of `x`, so the original dictionary has not been affected anyway. 170 | 171 | 172 | ### `map_object` 173 | 174 | In addition, `rounder` offers you a `map_object()` function, which enables you to run any function that takes a number and returns a number for all numbers in an object. This works like the following: 175 | 176 | ```python 177 | >>> xy = { 178 | ... "x": [12, 33.3, 45.5, 3543.22], 179 | ... "y": [.45, .3554, .55223, .9911], 180 | ... "expl": "x and y values" 181 | ... } 182 | >>> r.round_object( 183 | ... r.map_object( 184 | ... lambda x: x**3/(1 - 1/x), 185 | ... xy, 186 | ... use_copy=True), 187 | ... 4, 188 | ... use_copy=True 189 | ... ) 190 | {'x': [1885.0909, 38069.258, 96313.1475, 44495587353.9829], 'y': [-0.0746, -0.0248, -0.2077, -108.4126], 'expl': 'x and y values'} 191 | 192 | ``` 193 | 194 | You would have achieved the same result had you used `round` inside the `lambda` body: 195 | 196 | ```python 197 | >>> r.map_object(lambda x: round(x**3/(1 - 1/x), 4), xy, use_copy=True) 198 | {'x': [1885.0909, 38069.258, 96313.1475, 44495587353.9829], 'y': [-0.0746, -0.0248, -0.2077, -108.4126], 'expl': 'x and y values'} 199 | 200 | ``` 201 | 202 | The latter approach, actually, will be quicker, as the full recursion is used just once (by `r.map_object()`), not twice, as it was done in the former example (first, by `r.map_object()`, and then by `r.round_object()`). 203 | 204 | 205 | If the function takes additional arguments, you can use a wrapper function to overcome this issue: 206 | 207 | ```python 208 | >>> def forget(something): pass 209 | >>> def fun(x, to_forget): 210 | ... forget(to_forget) 211 | ... return x**2 212 | >>> def wrapper(x): 213 | ... return fun(x, "this can be forgotten") 214 | >>> r.map_object(wrapper, [2, 2, [3, 3, ], {"a": 5}]) 215 | [4, 4, [9, 9], {'a': 25}] 216 | 217 | ``` 218 | 219 | Or even: 220 | 221 | ```python 222 | >>> r.map_object( 223 | ... lambda x: fun(x, "this can be forgotten"), 224 | ... [2, 2, [3, 3, ], {"a": 5}] 225 | ... ) 226 | [4, 4, [9, 9], {'a': 25}] 227 | 228 | ``` 229 | 230 | # Types that `rounder` works with 231 | 232 | First of all, all these functions will work the very same way as their original counterparts (not for `signif`, which does not have one): 233 | 234 | ```python 235 | >>> import math 236 | >>> x = 12345.12345678901234567890 237 | >>> for d in range(10): 238 | ... assert round(x, d) == r.round_object(x, d) 239 | ... assert math.ceil(x) == r.ceil_object(x) 240 | ... assert math.floor(x) == r.floor_object(x) 241 | 242 | ``` 243 | 244 | The power of `rounder`, however, comes with working with many other types, and in particular, complex objects that contains them. `rounder` will work with the following types: 245 | 246 | * `int` 247 | * `float` 248 | * `complex` 249 | * `decimal.Decimal` 250 | * `fractions.Fraction` 251 | * `set` and `frozenset` 252 | * `list` 253 | * `tuple` 254 | * `collections.namedtuple` and `typing.NamedTuple` 255 | * `dict` 256 | * `collections.defaultdict`, `collections.OrderedDict` and `collections.UserDict` 257 | * `collections.Counter` 258 | * `collections.deque` 259 | * `array.array` 260 | * `map` 261 | * `filter` 262 | * generators and generator functions 263 | 264 | > Note that `rounder` will work with any type that follows the `collections.abc.Mapping` interface. 265 | 266 | > `collections.Counter`: Beware that using `rounder` for this type will affect the _values_ of the counter, which originally represent counts. In most cases, that would mean no effect on such counts (for `rounder.round_object()`, `rounder.ceil_object()` and `rounder.floor_object()`), but `rounder.signif_object()` and `rounder.map_object()` can change the counts. In rare situations, you can keep float values as values in the counter; in such situations, `rounder` will work as expected. 267 | 268 | > If `rounder` meets a type that is not recognized as any of the given above, it will simply return it untouched. 269 | 270 | > "Warning": In the case of `range` objects, generators and generator functions, the `rounder` functions will change the type of the object, returning a `map` object. This should not affect the final result the using these objects, unless you directly use their types somehow. 271 | 272 | 273 | ## Immutable types 274 | 275 | `rounder` does work with immutable types! It simply creates a new object, with rounded numbers: 276 | 277 | ```python 278 | >>> x = {1.12, 4.555} 279 | >>> r.round_object(x) 280 | {1.0, 5.0} 281 | >>> r.round_object(frozenset(x)) 282 | frozenset({1.0, 5.0}) 283 | >>> r.round_object((1.12, 4.555)) 284 | (1.0, 5.0) 285 | >>> r.round_object(({1.1, 1.2}, frozenset({1.444, 2.222}))) 286 | ({1.0}, frozenset({1.0, 2.0})) 287 | 288 | ``` 289 | 290 | So, note that it makes no difference whether you use `True` or `False` for `use_copy`, as with immutable types `rounder` will create a copy anyway. 291 | 292 | Remember, however, that in the case of sets, you can get a shorter set then the original one: 293 | 294 | ```python 295 | >>> x = {1.12, 1.99} 296 | >>> r.ceil_object(x) 297 | {2} 298 | 299 | ``` 300 | 301 | 302 | ## NumPy and Pandas 303 | 304 | `rounder` does not work with `numpy` and `pandas`: they have their own builtin methods for rounding, and using them will be much quicker. However, if for some reason a `rounder` function meets a `pandas` or a `numpy` object on its way, like here: 305 | 306 | ```python 307 | r.round_object(dict( 308 | values=np.array([1.223, 3.3332, 2.323]), 309 | something_else="whatever else" 310 | ) 311 | 312 | ``` 313 | 314 | then it will simply return the object untouched. 315 | 316 | 317 | # Testing 318 | 319 | The package is covered with unit `pytest`s, located in the [tests/ folder](tests/). In addition, the package uses `doctest`s, which are collected in this README and in the main module, [rounder.py](rounder/rounder.py). These `doctest`s serve mainly documentation purposes, and since they can be run any time during development and before each release, they help to check whether all the examples are correct and work fine. 320 | 321 | 322 | # OS 323 | 324 | The package is OS-independent. Its releases are checked in local machines, on Windows 10 and Ubuntu 20.04 for Windows, and in Pythonista for iPad. 325 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0", "setuptools-scm"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "rounder" 7 | version = "0.7.1" 8 | authors = [ 9 | { name = "Nyggus", email = "nyggus@gmail.com" }, 10 | { name = "Ruud van der Ham"}, 11 | ] 12 | description = "A tool for rounding numbers in complex Python objects" 13 | readme = "README.md" 14 | license = { file = "LICENSE" } 15 | requires-python = ">=3.8" 16 | classifiers = [ 17 | "Intended Audience :: Developers", 18 | "Programming Language :: Python :: 3", 19 | "Programming Language :: Python :: 3.8", 20 | "Programming Language :: Python :: 3.9", 21 | "Programming Language :: Python :: 3.10", 22 | "Programming Language :: Python :: 3.11", 23 | "License :: OSI Approved :: MIT License", 24 | "Operating System :: OS Independent", 25 | "Topic :: Software Development :: Libraries :: Python Modules", 26 | ] 27 | 28 | [project.urls] 29 | Homepage = "https://github.com/nyggus/rounder/" 30 | 31 | [tool.setuptools] 32 | packages = ["rounder"] 33 | 34 | [project.optional-dependencies] 35 | dev = ["wheel", "black", "pytest", "mypy", "setuptools", "build"] 36 | 37 | [tool.black] 38 | line-length = 79 39 | 40 | [tool.pytest.ini_options] 41 | testpaths = ["README.md", "tests", "rounder"] 42 | addopts = '--doctest-modules --doctest-glob="*.md"' 43 | doctest_optionflags = [ 44 | "ELLIPSIS", 45 | "NORMALIZE_WHITESPACE", 46 | "IGNORE_EXCEPTION_DETAIL" 47 | ] 48 | -------------------------------------------------------------------------------- /rounder/__init__.py: -------------------------------------------------------------------------------- 1 | from .rounder import ( 2 | map_object, 3 | round_object, 4 | floor_object, 5 | ceil_object, 6 | signif_object, 7 | signif, 8 | map_object_clean 9 | ) 10 | -------------------------------------------------------------------------------- /rounder/rounder.py: -------------------------------------------------------------------------------- 1 | import array 2 | import builtins 3 | import copy 4 | import math 5 | import types 6 | from collections import defaultdict 7 | from collections import deque 8 | from collections import OrderedDict 9 | from collections import defaultdict 10 | from collections import Counter 11 | from collections.abc import Mapping 12 | from collections.abc import Set 13 | from collections import UserList 14 | from numbers import Number 15 | from decimal import Decimal 16 | from fractions import Fraction 17 | from typing import Any, Callable, Dict, Optional, Union 18 | 19 | 20 | dispatch_table_store: Dict = {} 21 | 22 | 23 | def types_lookup(type_name: str) -> Optional[Any]: 24 | """Get a type from name. 25 | 26 | If type_name does not exist (because of the Python version), return None. 27 | """ 28 | return getattr(types, type_name, None) 29 | 30 | 31 | def _do(func, obj, digits, use_copy): 32 | 33 | if type(obj) in (float, int): 34 | return func(obj, *digits) 35 | 36 | def convert_number(obj): 37 | return func(obj, *digits) 38 | 39 | def convert_complex(obj): 40 | return convert(obj.real) + convert(obj.imag) * 1j 41 | 42 | def convert_list(obj): 43 | if use_copy: 44 | return type(obj)(map(convert, obj)) 45 | obj[:] = list(map(convert, obj)) 46 | return obj 47 | 48 | def convert_map(obj): 49 | if use_copy: 50 | obj_copy = copy.deepcopy(obj) 51 | return map(convert, obj_copy) 52 | return map(convert, obj) 53 | 54 | def convert_filter(obj): 55 | return filter( 56 | bool, convert_map(obj) 57 | ) # bool of map object is always True 58 | 59 | def convert_generator(obj): 60 | return map(convert, obj) 61 | 62 | def convert_namedtuple(obj): 63 | return obj._replace(**convert(obj._asdict())) 64 | 65 | def convert_dict(obj): 66 | if use_copy: 67 | return_obj = type(obj)() 68 | else: 69 | return_obj = obj 70 | for k, v in obj.items(): 71 | return_obj[k] = convert(v) 72 | 73 | return return_obj 74 | 75 | def convert_array(obj): 76 | if use_copy: 77 | return array.array(obj.typecode, convert(obj.tolist())) 78 | obj[:] = array.array(obj.typecode, convert(obj.tolist())) 79 | return obj 80 | 81 | def convert_deque(obj): 82 | if use_copy: 83 | return type(obj)(map(convert, obj)) 84 | for i, elem in enumerate(obj): 85 | obj[i] = convert(elem) 86 | return obj 87 | 88 | def convert_tuple_set_frozenset(obj): 89 | return type(obj)(map(convert, obj)) 90 | 91 | def convert_rest(obj): 92 | if isinstance(obj, Number): 93 | if isinstance(obj, complex): 94 | return type(obj)(convert_complex(obj)) 95 | return type(obj)(convert_number(obj)) 96 | if isinstance(obj, (list, UserList)): 97 | return type(obj)(convert_list(obj)) 98 | if isinstance(obj, tuple): 99 | if hasattr(obj, "_fields"): # it's a namedtuple 100 | return convert_namedtuple(obj) 101 | return convert_tuple_set_frozenset(obj) 102 | if isinstance(obj, Set): 103 | return convert_tuple_set_frozenset(obj) 104 | if isinstance(obj, Mapping): 105 | return convert_dict(obj) 106 | if isinstance(obj, array.array): 107 | return convert_array(obj) 108 | if isinstance(obj, deque): 109 | return convert_deque(obj) 110 | if isinstance(obj, map): 111 | return convert_map(obj) 112 | if hasattr(obj, "__dict__"): 113 | # placed at the end as some of the above (derived) types 114 | # might have a __dict__ 115 | if use_copy: 116 | obj_copy = copy.copy(obj) 117 | for k, v in obj_copy.__dict__.items(): 118 | obj_copy.__dict__[k] = convert(v) 119 | return obj_copy 120 | convert_dict(obj.__dict__) 121 | return obj 122 | 123 | return obj 124 | 125 | def convert(obj): 126 | if type(obj) in (float, int): 127 | return func(obj, *digits) 128 | 129 | return dispatch_table.get(type(obj), convert_rest)(obj) 130 | 131 | try: 132 | dispatch_table = dispatch_table_store[(func, *digits, use_copy)] 133 | except KeyError: 134 | dispatch_table = dispatch_table_store[(func, *digits, use_copy)] = { 135 | bool: lambda obj: func(obj, *digits), 136 | Decimal: lambda obj: func(obj, *digits), 137 | Fraction: lambda obj: func(obj, *digits), 138 | complex: lambda obj: convert(obj.real) + convert(obj.imag) * 1j, 139 | list: convert_list, 140 | tuple: lambda obj: tuple(map(convert, obj)), 141 | set: lambda obj: set(map(convert, obj)), 142 | frozenset: lambda obj: frozenset(map(convert, obj)), 143 | dict: convert_dict, 144 | defaultdict: convert_dict, 145 | OrderedDict: convert_dict, 146 | Counter: convert_dict, 147 | array.array: convert_array, 148 | deque: convert_deque, 149 | map: convert_map, 150 | filter: convert_filter, 151 | range: convert_map, 152 | str: lambda obj: obj, 153 | types_lookup("NoneType"): lambda obj: obj, 154 | types_lookup("GeneratorType"): convert_generator, 155 | types_lookup("FunctionType"): lambda obj: obj, 156 | types_lookup("LambdaType"): lambda obj: obj, 157 | types_lookup("CoroutineType"): lambda obj: obj, 158 | types_lookup("AsyncGeneratorType"): lambda obj: obj, 159 | types_lookup("CellType"): lambda obj: obj, 160 | types_lookup("MethodType"): lambda obj: obj, 161 | types_lookup("BuiltinFunctionType"): lambda obj: obj, 162 | types_lookup("BuiltinMethodType"): lambda obj: obj, 163 | types_lookup("MethodWrapperType"): lambda obj: obj, 164 | types_lookup("NotImplementedType"): lambda obj: obj, 165 | types_lookup("MethodDescriptorType"): lambda obj: obj, 166 | types_lookup("ClassMethodDescriptorType"): lambda obj: obj, 167 | types_lookup("EllipsisType"): lambda obj: obj, 168 | types_lookup("UnionType"): lambda obj: obj, 169 | types_lookup("FrameType"): lambda obj: obj, 170 | types_lookup("MemberDescriptorType"): lambda obj: obj, 171 | } 172 | 173 | try: 174 | return convert(obj) 175 | except Exception: 176 | return obj 177 | 178 | 179 | def signif(x: float, digits: int) -> float: 180 | """Round number to significant digits. 181 | Translated from Java algorithm available on 182 | Stack Overflow 183 | Args: 184 | x (float, int): a value to be rounded 185 | digits (int, optional): number of significant digits. Defaults to 3. 186 | Returns: 187 | float or int: x rounded to significant digits 188 | >>> signif(1.2222, 3) 189 | 1.22 190 | >>> signif(12222, 3) 191 | 12200 192 | >>> signif(1, 3) 193 | 1 194 | >>> signif(123.123123, 5) 195 | 123.12 196 | >>> signif(123.123123, 3) 197 | 123.0 198 | >>> signif(123.123123, 1) 199 | 100.0 200 | """ 201 | if x == 0: 202 | return 0 203 | if not isinstance(x, Number) or isinstance(x, complex): 204 | raise TypeError( 205 | "x must be a (non-complex) number, " 206 | f"not '{type(x).__name__}'" 207 | ) 208 | d = math.ceil(math.log10(abs(x))) 209 | power = digits - d 210 | magnitude = math.pow(10, power) 211 | shifted = builtins.round(x * magnitude) 212 | if x >= math.pow(10, digits): 213 | return type(x)(round(shifted / magnitude)) 214 | else: 215 | return type(x)(shifted / magnitude) 216 | 217 | 218 | def round_object(obj: Any, digits: int = 0, use_copy: bool = False) -> Any: 219 | """Round numbers in a Python object. 220 | Args: 221 | obj (any): any Python object 222 | digits (int, optional): number of digits. Defaults to 0. 223 | use_copy (bool, optional): use a deep copy or work with the original 224 | object? Defaults to False, in which case mutable objects (a list 225 | or a dict, for instance) will be affected inplace. In the case of 226 | unpickable objects, TypeError will be raised. 227 | Returns: 228 | any: the object with values rounded to requested number of digits 229 | >>> round_object(12.12, 1) 230 | 12.1 231 | >>> round_object("string", 1) 232 | 'string' 233 | >>> round_object(["Shout", "Bamalama"]) 234 | ['Shout', 'Bamalama'] 235 | >>> obj = {'number': 12.323, 'string': 'whatever', 'list': [122.45, .01]} 236 | >>> round_object(obj, 2) 237 | {'number': 12.32, 'string': 'whatever', 'list': [122.45, 0.01]} 238 | """ 239 | return _do(builtins.round, obj, [digits], use_copy) 240 | 241 | 242 | def ceil_object(obj: Any, use_copy: bool = False) -> Any: 243 | """Round numbers in a Python object, using the ceiling algorithm. 244 | This means rounding to the closest greater integer. 245 | Args: 246 | obj (any): any Python object 247 | use_copy (bool, optional): use a deep copy or work with the original 248 | object? Defaults to False, in which case mutable objects (a list 249 | or a dict, for instance) will be affect inplace. 250 | Returns: 251 | any: the object with values ceiling-rounded to requested number 252 | of digits 253 | >>> ceil_object(12.12) 254 | 13 255 | >>> ceil_object("string") 256 | 'string' 257 | >>> ceil_object(["Shout", "Bamalama"]) 258 | ['Shout', 'Bamalama'] 259 | >>> obj = {'number': 12.323, 'string': 'whatever', 'list': [122.45, .01]} 260 | >>> ceil_object(obj) 261 | {'number': 13, 'string': 'whatever', 'list': [123, 1]}""" 262 | return _do(math.ceil, obj, [], use_copy) 263 | 264 | 265 | def floor_object(obj: Any, use_copy: bool = False) -> Any: 266 | """Round numbers in a Python object, using the floor algorithm. 267 | This means rounding to the closest smaller integer. 268 | Args: 269 | obj (any): any Python object 270 | use_copy (bool, optional): use a deep copy or work with the original 271 | object? Defaults to False, in which case mutable objects (a list 272 | or a dict, for instance) will be affect inplace. 273 | Returns: 274 | any: the object with values floor-rounded to requested number of 275 | digits 276 | >>> floor_object(12.12) 277 | 12 278 | >>> floor_object("string") 279 | 'string' 280 | >>> floor_object(["Shout", "Bamalama"]) 281 | ['Shout', 'Bamalama'] 282 | >>> obj = {'number': 12.323, 'string': 'whatever', 'list': [122.45, .01]} 283 | >>> floor_object(obj) 284 | {'number': 12, 'string': 'whatever', 'list': [122, 0]} 285 | """ 286 | return _do(math.floor, obj, [], use_copy) 287 | 288 | 289 | def signif_object(obj: Any, digits: int = 3, use_copy: bool = False): 290 | """Round numbers in a Python object to requested significant digits. 291 | Args: 292 | obj (any): any Python object 293 | digits (int, optional): number of digits. 294 | use_copy (bool, optional): use a deep copy or work with the original 295 | object? Defaults to False, in which case mutable objects (a list 296 | or a dict, for instance) will be affect inplace. 297 | Returns: 298 | any: the object with values rounded to requested number of significant 299 | digits 300 | >>> signif_object(12.12, 3) 301 | 12.1 302 | >>> signif_object(.1212, 3) 303 | 0.121 304 | >>> signif_object(.00001212, 3) 305 | 1.21e-05 306 | >>> signif_object(.00001219, 3) 307 | 1.22e-05 308 | >>> signif_object(1212.0, 3) 309 | 1210.0 310 | >>> signif_object("string", 1) 311 | 'string' 312 | >>> signif_object(["Shout", "Bamalama"], 5) 313 | ['Shout', 'Bamalama'] 314 | >>> obj = {'number': 12.323, 'string': 'whatever', 'list': [122.45, .01]} 315 | >>> signif_object(obj, 3) 316 | {'number': 12.3, 'string': 'whatever', 'list': [122.0, 0.01]} 317 | """ 318 | return _do(signif, obj, [digits], use_copy) 319 | 320 | 321 | def map_object( 322 | map_function: Callable[[Number], Number], 323 | obj: Any, 324 | use_copy: bool = False, 325 | ): 326 | """Maps recursively a Python object to a given function. 327 | Args: 328 | map_function: function that converts a number and returns a number 329 | obj (any): any Python object 330 | use_copy (bool, optional): use a deep copy or work with the original 331 | object? Defaults to False, in which case mutable objects (a list 332 | or a dict, for instance) will be affect inplace. 333 | Returns: 334 | any: the object with values mapped with the given map_function 335 | >>> map_object(abs, -1) 336 | 1 337 | >>> map_object(abs, [-2, -1, 0, 1, 2]) 338 | [2, 1, 0, 1, 2] 339 | >>> round_object( 340 | ... map_object( 341 | ... math.sin, 342 | ... {0:0, 90: math.radians(90), 343 | ... 180: math.radians(180), 344 | ... 270: math.radians(270)} 345 | ... ), 346 | ... 3 347 | ... ) 348 | {0: 0.0, 90: 1.0, 180: 0.0, 270: -1.0} 349 | >>> map_object(abs, "string") 350 | 'string' 351 | >>> obj = {'number': 12.323, 'string': 'whatever', 'list': [122.45, .01]} 352 | >>> map_object(lambda x: signif(x**.5, 3), obj) 353 | {'number': 3.51, 'string': 'whatever', 'list': [11.1, 0.1]} 354 | """ 355 | if not callable(map_function): 356 | raise TypeError( 357 | "map_function must be a callable, " 358 | f"not {type(map_function).__name__}" 359 | ) 360 | return _do(map_function, obj, [], use_copy) 361 | 362 | 363 | def map_object_clean( 364 | map_function: Callable[[float], float], 365 | obj: Any, 366 | use_copy: bool = False, 367 | ): 368 | """Maps recursively a Python object to a given function. 369 | Args: 370 | map_function: function that converts a number and returns a number 371 | obj (any): any Python object 372 | use_copy (bool, optional): use a deep copy or work with the original 373 | object? Defaults to False, in which case mutable objects (a list 374 | or a dict, for instance) will be affect inplace. 375 | Returns: 376 | any: the object with values mapped with the given map_function 377 | >>> map_object(abs, -1) 378 | 1 379 | >>> map_object(abs, [-2, -1, 0, 1, 2]) 380 | [2, 1, 0, 1, 2] 381 | >>> round_object( 382 | ... map_object( 383 | ... math.sin, 384 | ... {0:0, 90: math.radians(90), 385 | ... 180: math.radians(180), 386 | ... 270: math.radians(270)} 387 | ... ), 388 | ... 3 389 | ... ) 390 | {0: 0.0, 90: 1.0, 180: 0.0, 270: -1.0} 391 | >>> map_object(abs, "string") 392 | 'string' 393 | >>> obj = {'number': 12.323, 'string': 'whatever', 'list': [122.45, .01]} 394 | >>> map_object(lambda x: signif(x**.5, 3), obj) 395 | {'number': 3.51, 'string': 'whatever', 'list': [11.1, 0.1]} 396 | """ 397 | return _do(map_function, obj, [], use_copy) 398 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyggus/rounder/e550d4a7450e3b4d97126deef3db25c7fed910b6/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import decimal 2 | import fractions 3 | import random 4 | from copy import deepcopy 5 | 6 | import pytest 7 | 8 | 9 | N_OF_TESTS = 3 10 | LIMITS = [(-500, 500), (-0.1, 0.1)] 11 | N_OF_DIGITS = range(7) 12 | OBJECT_LENGTHS = [10, 100, 1000, 100_000] 13 | 14 | 15 | @pytest.fixture(params=[tuple, set, list]) 16 | def iter_type(request): 17 | return request.param 18 | 19 | 20 | @pytest.fixture(params=OBJECT_LENGTHS) 21 | def length(request): 22 | return request.param 23 | 24 | 25 | @pytest.fixture(params=[None] * N_OF_TESTS) 26 | def randomize(request): 27 | return request.param 28 | 29 | 30 | @pytest.fixture(params=LIMITS) 31 | def limits(request): 32 | return request.param 33 | 34 | 35 | @pytest.fixture 36 | def x(randomize, limits): 37 | return random.uniform(*limits) 38 | 39 | 40 | @pytest.fixture(scope="session", params=N_OF_DIGITS) 41 | def n_of_digits(request): 42 | return request.param 43 | 44 | 45 | @pytest.fixture() 46 | def complex_object(): 47 | obj = { 48 | "a": 12.22221111, 49 | "string": "something nice, ha?", 50 | "callable": lambda x: x ** 2, 51 | "b": 2, 52 | "c": 1.222, 53 | "d": [1.12343, 0.023492], 54 | "e": { 55 | "ea": 1 / 44, 56 | "eb": {1.333, 2.999}, 57 | "ec": dict(eca=1.565656, ecb=1.765765765), 58 | }, 59 | } 60 | return deepcopy(obj) 61 | 62 | 63 | @pytest.fixture() 64 | def object_with_various_numbers(): 65 | obj = { 66 | "complex number": 1.2 - 2j, 67 | "string": "something nice, ha?", 68 | "callable": lambda x: x ** 2, 69 | "decimal": decimal.Decimal("1.2"), 70 | "fraction": fractions.Fraction(1, 4), 71 | } 72 | return deepcopy(obj) 73 | 74 | 75 | @pytest.fixture( 76 | params=[ 77 | [222.222, 333.333, 1.000045, "Shout Bamalama!"], 78 | [222.222, [333.333, 444.444, 5555.5555], 1.000045, "Shout Bamalama!"], 79 | ] 80 | ) 81 | def obj_list(request): 82 | return request.param 83 | 84 | 85 | @pytest.fixture( 86 | params=[ 87 | {"a": 222.222, "b": 333.333, "c": 1.000045, "d": "Shout Bamalama!"}, 88 | { 89 | "a": 222.222, 90 | "b": [333.333, 444.444, 5555.5555], 91 | "c": 1.000045, 92 | "d": "Shout Bamalama!", 93 | }, 94 | ] 95 | ) 96 | def obj_dict(request): 97 | return request.param 98 | 99 | 100 | @pytest.fixture( 101 | params=[ 102 | {222.222, 333.333, 1.000045, "Shout Bamalama!"}, 103 | {222.222, (333.333, 444.444, 5555.5555), 1.000045, "Shout Bamalama!"}, 104 | ] 105 | ) 106 | def obj_set(request): 107 | return request.param 108 | 109 | 110 | @pytest.fixture( 111 | params=[ 112 | (222.222, 333.333, 1.000045, "Shout Bamalama!"), 113 | (222.222, (333.333, 444.444, 5555.5555), 1.000045, "Shout Bamalama!"), 114 | ] 115 | ) 116 | def obj_tuple(request): 117 | return request.param 118 | -------------------------------------------------------------------------------- /tests/test_rounder_functions.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import decimal 3 | import fractions 4 | import pytest 5 | import random 6 | import rounder as r 7 | 8 | from numbers import Number 9 | from copy import deepcopy 10 | from math import ceil, floor 11 | 12 | 13 | def test_randomized_tests_round_ceil_floor_object(x, n_of_digits): 14 | assert round(x, n_of_digits) == r.round_object(x, n_of_digits) 15 | assert ceil(x) == r.ceil_object(x) 16 | assert floor(x) == r.floor_object(x) 17 | 18 | 19 | def test_use_copy_lists(obj_list): 20 | rounded_copy = r.round_object(obj_list, 1, True) 21 | assert rounded_copy != obj_list 22 | rounded_no_copy = r.round_object(obj_list, 1, False) 23 | assert rounded_no_copy == rounded_copy 24 | assert rounded_no_copy is not rounded_copy 25 | assert rounded_no_copy is obj_list 26 | 27 | 28 | def test_use_copy_dicts(obj_dict): 29 | rounded_copy = r.round_object(obj_dict, 1, True) 30 | assert rounded_copy != obj_dict 31 | rounded_no_copy = r.round_object(obj_dict, 1, False) 32 | assert rounded_no_copy == rounded_copy 33 | assert rounded_no_copy is not rounded_copy 34 | assert rounded_no_copy is obj_dict 35 | 36 | 37 | def test_no_copy_with_lists(obj_list): 38 | rounded = r.round_object(obj_list, 1) 39 | assert rounded is obj_list 40 | 41 | 42 | def test_no_copy_with_dicts(obj_dict): 43 | rounded = r.round_object(obj_dict, 1) 44 | assert rounded is obj_dict 45 | 46 | 47 | def test_copy_with_tuples(obj_tuple): 48 | x_rounded = r.round_object(obj_tuple, 1, True) 49 | assert x_rounded is not obj_tuple 50 | assert x_rounded != obj_tuple 51 | 52 | 53 | def test_copy_with_sets(obj_set): 54 | x_rounded = r.round_object(obj_set, 1, True) 55 | assert x_rounded is not obj_set 56 | assert x_rounded != obj_set 57 | 58 | 59 | def test_no_copy_with_tuples(obj_tuple): 60 | x_rounded = r.round_object(obj_tuple, 1, False) 61 | assert x_rounded is not obj_tuple 62 | assert x_rounded != obj_tuple 63 | 64 | 65 | def test_no_copy_with_sets(obj_set): 66 | x_rounded = r.round_object(obj_set, 1, False) 67 | assert x_rounded is not obj_set 68 | assert x_rounded != obj_set 69 | 70 | 71 | def test_randomized_tests_using_copy_lists( 72 | randomize, iter_type, length, limits, n_of_digits 73 | ): 74 | x = iter_type(random.uniform(*limits) for i in range(length)) 75 | x_copy = deepcopy(x) 76 | 77 | r_rounded_x = r.round_object(x, n_of_digits, use_copy=True) 78 | assert r_rounded_x != x 79 | 80 | rounded_x = iter_type(round(x, n_of_digits) for x in x_copy) 81 | assert r_rounded_x is not x 82 | assert rounded_x == r_rounded_x 83 | 84 | # use_copy was used, so the original list did not change: 85 | # (here, this makes a difference for lists but not for 86 | # sets and tuples) 87 | if iter_type is list: 88 | assert r_rounded_x != x 89 | no_copy_r_rounded_x = r.round_object(x, n_of_digits, use_copy=False) 90 | assert no_copy_r_rounded_x == r_rounded_x 91 | 92 | # use_copy was NOT used, so the original list DID change: 93 | assert r_rounded_x == x 94 | 95 | 96 | def test_randomized_tests_using_copy_dicts( 97 | randomize, length, limits, n_of_digits 98 | ): 99 | x = {str(i): random.uniform(*limits) for i in range(length)} 100 | x_copy = deepcopy(x) 101 | 102 | r_rounded_x = r.round_object(x, n_of_digits, use_copy=True) 103 | assert r_rounded_x != x 104 | 105 | rounded_x = {letter: round(x, n_of_digits) for letter, x in x_copy.items()} 106 | assert r_rounded_x is not x 107 | assert rounded_x == r_rounded_x 108 | 109 | # use_copy was used, so the original list did not change: 110 | assert r_rounded_x != x 111 | no_copy_r_rounded_x = r.round_object(x, n_of_digits, use_copy=False) 112 | assert no_copy_r_rounded_x == r_rounded_x 113 | 114 | # use_copy was NOT used, so the original list DID change: 115 | assert r_rounded_x == x 116 | 117 | 118 | def test_with_non_roundable_items(): 119 | assert r.round_object("Shout Bamalama!") == "Shout Bamalama!" 120 | assert r.ceil_object("Shout Bamalama!") == "Shout Bamalama!" 121 | assert r.floor_object("Shout Bamalama!") == "Shout Bamalama!" 122 | assert r.signif_object("Shout Bamalama!", 5) == "Shout Bamalama!" 123 | 124 | 125 | def test_with_non_roundable_items_lists(): 126 | assert r.round_object(["Shout Bamalama!"]) == ["Shout Bamalama!"] 127 | assert r.ceil_object(["Shout Bamalama!"]) == ["Shout Bamalama!"] 128 | assert r.floor_object(["Shout Bamalama!"]) == ["Shout Bamalama!"] 129 | assert r.signif_object(["Shout Bamalama!"], 5) == ["Shout Bamalama!"] 130 | 131 | 132 | def test_with_non_roundable_items_tuples(): 133 | assert r.round_object(("Shout Bamalama!",)) == ("Shout Bamalama!",) 134 | assert r.ceil_object(("Shout Bamalama!",)) == ("Shout Bamalama!",) 135 | assert r.floor_object(("Shout Bamalama!",)) == ("Shout Bamalama!",) 136 | assert r.signif_object(("Shout Bamalama!",), 5) == ("Shout Bamalama!",) 137 | 138 | 139 | def test_with_non_roundable_items_sets(): 140 | assert r.round_object({"Shout Bamalama!"}) == {"Shout Bamalama!"} 141 | assert r.ceil_object({"Shout Bamalama!"}) == {"Shout Bamalama!"} 142 | assert r.floor_object({"Shout Bamalama!"}) == {"Shout Bamalama!"} 143 | assert r.signif_object({"Shout Bamalama!"}, 5) == {"Shout Bamalama!"} 144 | 145 | 146 | def test_with_non_roundable_items_dicts(): 147 | assert r.round_object({"phrase": "Shout Bamalama!"}) == { 148 | "phrase": "Shout Bamalama!" 149 | } 150 | assert r.ceil_object({"phrase": "Shout Bamalama!"}) == { 151 | "phrase": "Shout Bamalama!" 152 | } 153 | assert r.floor_object({"phrase": "Shout Bamalama!"}) == { 154 | "phrase": "Shout Bamalama!" 155 | } 156 | assert r.signif_object({"phrase": "Shout Bamalama!"}, 5) == { 157 | "phrase": "Shout Bamalama!" 158 | } 159 | 160 | 161 | def test_round_object_for_complex_object(complex_object): 162 | rounded_complex_object = r.round_object(complex_object, 3, use_copy=True) 163 | assert rounded_complex_object is not complex_object 164 | assert rounded_complex_object["a"] == 12.222 165 | assert rounded_complex_object["e"] == { 166 | "ea": 0.023, 167 | "eb": {1.333, 2.999}, 168 | "ec": {"eca": 1.566, "ecb": 1.766}, 169 | } 170 | assert rounded_complex_object["d"] == [1.123, 0.023] 171 | 172 | 173 | def test_ceil_object_for_complex_object(complex_object): 174 | rounded_complex_object = r.ceil_object(complex_object, use_copy=True) 175 | assert rounded_complex_object is not complex_object 176 | assert rounded_complex_object["a"] == 13 177 | assert rounded_complex_object["e"] == { 178 | "ea": 1, 179 | "eb": {2, 3}, 180 | "ec": {"eca": 2, "ecb": 2}, 181 | } 182 | assert rounded_complex_object["d"] == [2, 1] 183 | 184 | 185 | def test_floor_object_for_complex_object(complex_object): 186 | rounded_complex_object = r.floor_object(complex_object, use_copy=True) 187 | assert rounded_complex_object is not complex_object 188 | assert rounded_complex_object["a"] == 12 189 | assert rounded_complex_object["e"] == { 190 | "ea": 0, 191 | "eb": {1, 2}, 192 | "ec": {"eca": 1, "ecb": 1}, 193 | } 194 | assert rounded_complex_object["d"] == [1, 0] 195 | 196 | 197 | def test_signif_object_for_complex_object_3_digits(complex_object): 198 | rounded_complex_object = r.signif_object(complex_object, 3, use_copy=True) 199 | assert rounded_complex_object is not complex_object 200 | assert rounded_complex_object["a"] == 12.2 201 | assert rounded_complex_object["e"] == { 202 | "ea": 0.0227, 203 | "eb": {1.33, 3.0}, 204 | "ec": {"eca": 1.57, "ecb": 1.77}, 205 | } 206 | assert rounded_complex_object["d"] == [1.12, 0.0235] 207 | 208 | 209 | def test_signif_object_for_complex_object_5_digits(complex_object): 210 | rounded_complex_object = r.signif_object(complex_object, 4, use_copy=True) 211 | assert rounded_complex_object is not complex_object 212 | assert rounded_complex_object["a"] == 12.22 213 | assert rounded_complex_object["e"] == { 214 | "ea": 0.02273, 215 | "eb": {1.333, 2.999}, 216 | "ec": {"eca": 1.566, "ecb": 1.766}, 217 | } 218 | assert rounded_complex_object["d"] == [1.123, 0.02349] 219 | 220 | 221 | def test_for_complex_numbers(): 222 | assert r.round_object(1.934643 - 2j, 2) == 1.93 - 2j 223 | assert r.ceil_object(1.934643 - 2j) == 2 - 2j 224 | assert r.floor_object(1.934643 - 2j) == 1 - 2j 225 | assert r.signif_object(1.934643 - 2j, 5) == 1.9346 - 2j 226 | 227 | 228 | def test_signif_floats(): 229 | assert r.signif(1.444555, 1) == 1.0 230 | assert r.signif(1.444555, 2) == 1.4 231 | assert r.signif(1.444555, 3) == 1.44 232 | assert r.signif(1.444555, 4) == 1.445 233 | assert r.signif(1.444555, 5) == 1.4446 234 | assert r.signif(1.444555, 6) == 1.44456 235 | assert r.signif(1.444555, 7) == 1.444555 236 | assert r.signif(1.444555, 8) == 1.444555 237 | 238 | 239 | def test_signif_ints(): 240 | assert r.signif(1444555, 1) == 1000000 241 | assert r.signif(1444555, 2) == 1400000 242 | assert r.signif(1444555, 3) == 1440000 243 | assert r.signif(1444555, 4) == 1445000 244 | assert r.signif(1444555, 5) == 1444600 245 | assert r.signif(1444555, 6) == 1444560 246 | assert r.signif(1444555, 7) == 1444555 247 | assert r.signif(1444555, 8) == 1444555 248 | 249 | 250 | def test_signif_exception(): 251 | with pytest.raises(TypeError, match="x must be a"): 252 | r.signif("string", 3) 253 | 254 | with pytest.raises(TypeError, match="x must be a"): 255 | r.signif([2.12], 3) 256 | 257 | with pytest.raises(TypeError, match="x must be a"): 258 | r.signif((1,), 3) 259 | 260 | 261 | def test_with_callable(): 262 | def f(x): 263 | return x 264 | 265 | assert callable(r.round_object(f)) 266 | 267 | def f(): 268 | pass 269 | 270 | x = {"items": [1.1222, 1.343434], "function": f} 271 | assert callable(x["function"]) 272 | 273 | x_rounded = r.round_object(x, 2, use_copy=True) 274 | assert callable(x_rounded["function"]) 275 | 276 | x_ceil = r.ceil_object(x, use_copy=True) 277 | assert callable(x_ceil["function"]) 278 | 279 | x_floor = r.floor_object(x, use_copy=True) 280 | assert callable(x_floor["function"]) 281 | 282 | x_signif = r.signif_object(x, 3, use_copy=True) 283 | assert callable(x_signif["function"]) 284 | 285 | 286 | def test_map_object_basic(): 287 | x = [1.12, 1.13] 288 | assert r.map_object(lambda x: x, x, False) is x 289 | assert r.map_object(lambda x: x, x, True) is not x 290 | assert r.map_object(lambda x: x, x, True) == x 291 | 292 | 293 | def test_map_object_exception(): 294 | with pytest.raises(TypeError, match="map_function"): 295 | r.map_object(2, 2) 296 | with pytest.raises(TypeError, match="map_function"): 297 | r.map_object((lambda x: x)(2), 2) 298 | with pytest.raises(TypeError, match="map_function"): 299 | r.map_object([], [2, 2]) 300 | 301 | 302 | def test_map_object_dicts_lists(): 303 | obj = { 304 | "list": [1.44, 2.67, 3.334, 6.665], 305 | "main value": 5.55, 306 | "explanation": "to be filled in", 307 | } 308 | negative_reversed_obj = r.signif_object( 309 | r.map_object(lambda x: -1 / x, obj, True), 5 310 | ) 311 | assert negative_reversed_obj == { 312 | "list": [-0.69444, -0.37453, -0.29994, -0.15004], 313 | "main value": -0.18018, 314 | "explanation": "to be filled in", 315 | } 316 | 317 | 318 | def test_map_object_squared(complex_object): 319 | squared_complex_object = r.round_object( 320 | r.map_object(lambda x: x * x, complex_object, True), 3 321 | ) 322 | assert squared_complex_object["a"] == 149.382 323 | assert squared_complex_object["e"] == { 324 | "ea": 0.001, 325 | "eb": {8.994, 1.777}, 326 | "ec": {"eca": 2.451, "ecb": 3.118}, 327 | } 328 | assert squared_complex_object["d"] == [1.262, 0.001] 329 | 330 | 331 | def test_map_Number(object_with_various_numbers): 332 | def square(x: Number) -> Number: 333 | return x ** 2 334 | 335 | squared_object = r.map_object(square, object_with_various_numbers) 336 | assert squared_object["complex number"] == (1.44 + 4j) 337 | assert squared_object["decimal"] == decimal.Decimal("1.44") 338 | assert squared_object["fraction"] == fractions.Fraction(1, 16) 339 | 340 | 341 | def test_with_class_instance(): 342 | class A: 343 | def __init__(self, x, y): 344 | self.x = x 345 | self.y = y 346 | 347 | a = A( 348 | x=10.0001, 349 | y=(44.4444, 55.5555, 66.6666, {"item1": 456.654, "item2": 90.0004}), 350 | ) 351 | assert r.round_object(a.x, 0, True) == 10 352 | assert r.round_object(a.y[:3], 0, True) == (44, 56, 67) 353 | assert r.round_object(a.y[3], 0, True) == {"item1": 457, "item2": 90} 354 | 355 | a_copy = r.signif_object(a, 4, True) 356 | assert a_copy.x == 10 357 | assert a_copy.y[:3] == (44.44, 55.56, 66.67) 358 | assert a_copy.y[3] == {"item1": 456.7, "item2": 90} 359 | 360 | # Note that you cannot create a copy of a class instance's attribute 361 | # and change it inplace in the instance: 362 | r.map_object(lambda x: x * 2, a_copy.x) 363 | assert a_copy.x == 10 364 | 365 | r.ceil_object(a) 366 | assert a.x == 11 367 | assert a.y[:3] == (45, 56, 67) 368 | assert a.y[3] == {"item1": 457, "item2": 91} 369 | 370 | 371 | def test_for_namedtuple(): 372 | from collections import namedtuple 373 | 374 | X = namedtuple("X", "a b c") 375 | 376 | x1 = X(12.12, 13.13, 14.94) 377 | x1_rounded_copy = r.round_object(x1, 0, True) 378 | assert x1_rounded_copy == X(12, 13, 15) 379 | x1_rounded_no_copy = r.round_object(x1, 0, False) 380 | assert x1_rounded_no_copy == X(12, 13, 15) 381 | assert x1 == X(12.12, 13.13, 14.94) 382 | 383 | x2 = X( 384 | a=12.13, 385 | b=[44.4444, 55.5555, 66.6666], 386 | c={"item1": 457.1212, "item2": 90.0001}, 387 | ) 388 | 389 | assert r.round_object(x2.a, 0) == 12 390 | assert r.round_object(x2.b, 0) == [44, 56, 67] 391 | assert r.round_object(x2.c, 0) == {"item1": 457, "item2": 90} 392 | 393 | assert r.signif_object(x2, 4) == X( 394 | a=12.13, b=[44.0, 56.0, 67.0], c={"item1": 457.0, "item2": 90.0} 395 | ) 396 | 397 | assert r.map_object(lambda x: r.signif(x * 2, 2), x2) == X( 398 | a=24.0, b=[88.0, 110.0, 130.0], c={"item1": 910.0, "item2": 180.0} 399 | ) 400 | 401 | 402 | def test_for_NamedTuple(): 403 | from typing import NamedTuple 404 | 405 | X1 = NamedTuple("X1", [("a", float), ("b", tuple), ("c", dict)]) 406 | 407 | x1 = X1(12.12, 13.13, 14.94) 408 | x1_rounded_copy = r.round_object(x1, 0, True) 409 | assert x1_rounded_copy == X1(12, 13, 15) 410 | x1_rounded_no_copy = r.round_object(x1, 0, False) 411 | assert x1_rounded_no_copy == X1(12, 13, 15) 412 | assert x1 == X1(12.12, 13.13, 14.94) 413 | 414 | X2 = NamedTuple("X2", [("a", float), ("b", tuple), ("c", dict)]) 415 | x2 = X2( 416 | a=12.13, 417 | b=[44.4444, 55.5555, 66.6666], 418 | c={"item1": 457.1212, "item2": 90.0001}, 419 | ) 420 | 421 | assert r.round_object(x2.a, 0) == 12 422 | assert r.round_object(x2.b, 0) == [44, 56, 67] 423 | assert r.round_object(x2.c, 0) == {"item1": 457, "item2": 90} 424 | 425 | assert r.signif_object(x2, 4) == X2( 426 | a=12.13, b=[44.0, 56.0, 67.0], c={"item1": 457.0, "item2": 90.0} 427 | ) 428 | 429 | assert r.map_object(lambda x: r.signif(x * 2, 2), x2) == X2( 430 | a=24.0, b=[88.0, 110.0, 130.0], c={"item1": 910.0, "item2": 180.0} 431 | ) 432 | 433 | 434 | def test_for_OrderedDict(): 435 | from collections import OrderedDict 436 | 437 | d = OrderedDict(a=1.1212, b=55.559) 438 | 439 | d_rounded_copy = r.round_object(d, 2, True) 440 | assert d_rounded_copy == OrderedDict(a=1.12, b=55.56) 441 | assert d == OrderedDict(a=1.1212, b=55.559) 442 | 443 | d_rounded_no_copy = r.round_object(d, 2, False) 444 | assert d_rounded_no_copy == OrderedDict(a=1.12, b=55.56) 445 | assert d == OrderedDict(a=1.12, b=55.56) 446 | 447 | d = OrderedDict( 448 | a=1.1212, 449 | b=55.559, 450 | c={"item1": "string", "item2": 3434.3434}, 451 | d=OrderedDict(d1=3434.3434, d2=[99.99, 1.2323 - 2j]), 452 | ) 453 | 454 | d_rounded_copy = r.round_object(d, 2, True) 455 | assert d_rounded_copy == OrderedDict( 456 | [ 457 | ("a", 1.12), 458 | ("b", 55.56), 459 | ("c", {"item1": "string", "item2": 3434.34}), 460 | ( 461 | "d", 462 | OrderedDict([("d1", 3434.34), ("d2", [99.99, (1.23 - 2j)])]), 463 | ), 464 | ] 465 | ) 466 | assert d != d_rounded_copy 467 | 468 | d_rounded_no_copy = r.round_object(d, 2, False) 469 | assert d_rounded_no_copy == d_rounded_copy 470 | assert d_rounded_no_copy == d 471 | 472 | 473 | def test_for_defaultdict(): 474 | from collections import defaultdict 475 | 476 | d = defaultdict(list) 477 | d["a"] = 1.1212 478 | d["b"] = 55.55656 479 | d["c"] = 0.104343 480 | 481 | d_rounded_copy = r.round_object(d, 2, True) 482 | 483 | assert d_rounded_copy == defaultdict(a=1.12, b=55.56, c=0.10) 484 | assert d != d_rounded_copy 485 | 486 | d_rounded_no_copy = r.round_object(d, 2, False) 487 | assert d_rounded_no_copy == defaultdict(a=1.12, b=55.56, c=0.10) 488 | assert d == d_rounded_no_copy 489 | 490 | 491 | def test_for_UserDict(): 492 | from collections import UserDict 493 | 494 | d = UserDict(dict(a=1.1212, b=55.559)) 495 | 496 | d_rounded_copy = r.round_object(d, 2, True) 497 | assert d_rounded_copy == UserDict(dict(a=1.12, b=55.56)) 498 | assert d == UserDict(dict(a=1.1212, b=55.559)) 499 | assert type(d_rounded_copy) == UserDict 500 | 501 | d_rounded_no_copy = r.round_object(d, 2, False) 502 | assert d_rounded_no_copy == UserDict(dict(a=1.12, b=55.56)) 503 | assert d == UserDict(dict(a=1.12, b=55.56)) 504 | 505 | d = UserDict( 506 | dict( 507 | a=1.1212, 508 | b=55.559, 509 | c={"item1": "string", "item2": 3434.3434}, 510 | d=UserDict(dict(d1=3434.3434, d2=[99.996, 1.2323 - 2j])), 511 | ) 512 | ) 513 | 514 | d_rounded_copy = r.round_object(d, 2, True) 515 | d_rounded_copy = UserDict( 516 | dict( 517 | a=1.12, 518 | b=55.56, 519 | c={"item1": "string", "item2": 3434.34}, 520 | d=UserDict(dict(d1=3434.34, d2=[100.0, 1.23 - 2j])), 521 | ) 522 | ) 523 | 524 | assert d != d_rounded_copy 525 | assert type(d_rounded_copy) == UserDict 526 | 527 | d_rounded_no_copy = r.round_object(d, 2, False) 528 | assert d_rounded_no_copy == d_rounded_copy 529 | assert d_rounded_no_copy == d 530 | 531 | 532 | def test_for_deque(): 533 | from collections import deque 534 | 535 | d = deque([1.1222, 3.9989, 4.005]) 536 | 537 | d_rounded_copy = r.round_object(d, 2, True) 538 | 539 | assert d_rounded_copy == deque([1.12, 4.0, 4.0]) 540 | assert d != d_rounded_copy 541 | 542 | d_rounded_no_copy = r.round_object(d, 2, False) 543 | assert d_rounded_no_copy == deque([1.12, 4.0, 4.0]) 544 | assert d == d_rounded_no_copy 545 | 546 | 547 | def test_for_Counter(): 548 | from collections import Counter 549 | 550 | d = Counter([1.1222, 1.2222, 4.005]) 551 | 552 | d_rounded_copy = r.map_object(lambda x: 2 * x, d, True) 553 | assert d_rounded_copy == Counter({1.1222: 2, 1.2222: 2, 4.005: 2}) 554 | 555 | d_rounded_no_copy = r.map_object(lambda x: 2 * x, d, False) 556 | assert d_rounded_no_copy is d 557 | 558 | d = Counter([1, 1, 2] + [5] * 12222) 559 | d_rounded_copy = r.round_object(d, 2, True) 560 | assert d_rounded_copy == d 561 | d_rounded_copy = r.signif_object(d, 2, True) 562 | assert d_rounded_copy != d 563 | assert d_rounded_copy == Counter({1: 2, 2: 1, 5: 12000}) 564 | 565 | 566 | def test_copy_for_frozenset(): 567 | x = frozenset( 568 | [ 569 | 222.222, 570 | 333.333, 571 | 1.000045, 572 | "Shout Bamalama!", 573 | 222.222, 574 | 333.333, 575 | 1.000045, 576 | "Shout Bamalama", 577 | ] 578 | ) 579 | 580 | x_rounded = r.round_object(x, 1, True) 581 | assert x_rounded is not x 582 | assert x_rounded != x 583 | 584 | 585 | def test_no_copy_for_frozenset(): 586 | x = frozenset( 587 | [ 588 | 222.222, 589 | 333.333, 590 | 1.000045, 591 | "Shout Bamalama!", 592 | 222.222, 593 | 333.333, 594 | 1.000045, 595 | "Shout Bamalama", 596 | ] 597 | ) 598 | 599 | x_rounded = r.round_object(x, 1, False) 600 | assert x_rounded is not x 601 | assert x_rounded == frozenset( 602 | {1.0, "Shout Bamalama!", 333.3, "Shout Bamalama", 222.2} 603 | ) 604 | 605 | 606 | def test_copy_for_bool(): 607 | x = True 608 | x_rounded = r.round_object(x, use_copy=True) 609 | assert x_rounded is not x 610 | assert x_rounded 611 | 612 | y = False 613 | y_rounded = r.round_object(y, use_copy=True) 614 | assert y_rounded is not y 615 | assert not y_rounded 616 | 617 | 618 | def test_no_copy_for_bool(): 619 | x = True 620 | x_rounded = r.round_object(x, use_copy=False) 621 | assert x_rounded is not x 622 | assert x_rounded 623 | 624 | y = False 625 | y_rounded = r.round_object(y, use_copy=False) 626 | assert y_rounded is not y 627 | assert not y_rounded 628 | 629 | 630 | def test_copy_for_Decimal(): 631 | from decimal import Decimal 632 | 633 | x = Decimal(1) / Decimal(7) 634 | x_rounded = r.round_object(x, 4, use_copy=True) 635 | assert x is not x_rounded 636 | assert x_rounded == Decimal("0.1429") 637 | 638 | 639 | def test_no_copy_for_Decimal(): 640 | from decimal import Decimal 641 | 642 | x = Decimal(1) / Decimal(7) 643 | x_rounded = r.round_object(x, 4, use_copy=False) 644 | assert x is not x_rounded 645 | assert x_rounded == Decimal("0.1429") 646 | 647 | 648 | def test_copy_for_Fraction(): 649 | from fractions import Fraction 650 | 651 | x = Fraction(1, 7) 652 | x_rounded = r.round_object(x, 4, use_copy=True) 653 | assert x is not x_rounded 654 | assert x_rounded == Fraction(1429, 10000) 655 | 656 | 657 | def test_no_copy_for_Fraction(): 658 | from fractions import Fraction 659 | 660 | x = Fraction(1, 7) 661 | x_rounded = r.round_object(x, 4, use_copy=False) 662 | assert x is not x_rounded 663 | assert x_rounded == Fraction(1429, 10000) 664 | 665 | 666 | def test_copy_for_None(): 667 | x = None 668 | x_rounded = r.round_object(x, 4, use_copy=True) 669 | assert x is x_rounded # as both are None 670 | 671 | 672 | def test_no_copy_for_None(): 673 | x = None 674 | x_rounded = r.round_object(x, 4, use_copy=False) 675 | assert x is x_rounded 676 | 677 | 678 | def test_copy_for_coroutine(): 679 | async def ff(): 680 | await asyncio.sleep(0.05) 681 | 682 | x_rounded = r.round_object(ff, use_copy=True) 683 | assert x_rounded is ff 684 | 685 | 686 | def test_no_copy_for_coroutine(): 687 | async def ff(): 688 | await asyncio.sleep(0.05) 689 | 690 | x_rounded = r.round_object(ff, use_copy=False) 691 | assert x_rounded is ff # copies are not created in the case of coroutines 692 | 693 | 694 | def test_copy_for_BuiltinFunctionType(): 695 | x = sum 696 | x_rounded = r.round_object(x, use_copy=True) 697 | assert x_rounded is sum 698 | 699 | 700 | def test_no_copy_for_BuiltinFunctionType(): 701 | x = sum 702 | x_rounded = r.round_object(x, use_copy=False) 703 | assert x_rounded is sum 704 | 705 | 706 | def test_copy_for_NotImplementedType(): 707 | x = NotImplemented 708 | x_rounded = r.round_object(x, use_copy=True) 709 | assert x_rounded is NotImplemented 710 | 711 | 712 | def test_no_copy_for_NotImplementedType(): 713 | x = NotImplemented 714 | x_rounded = r.round_object(x, use_copy=False) 715 | assert x_rounded is NotImplemented 716 | 717 | 718 | def test_copy_for_MethodDescriptorType(): 719 | x = str.join 720 | x_rounded = r.round_object(x, use_copy=True) 721 | assert x_rounded is x 722 | 723 | 724 | def test_no_copy_for_MethodDescriptorType(): 725 | x = str.join 726 | x_rounded = r.round_object(x, use_copy=False) 727 | assert x_rounded is x 728 | 729 | 730 | def test_copy_for_EllipsisType(): 731 | x = ... 732 | x_rounded = r.round_object(x, use_copy=True) 733 | assert x_rounded is x 734 | 735 | 736 | def test_no_copy_for_EllipsisType(): 737 | x = ... 738 | x_rounded = r.round_object(x, use_copy=False) 739 | assert x_rounded is x 740 | 741 | 742 | def test_no_copy_for_FrameType(): 743 | import inspect 744 | 745 | x = inspect.currentframe() 746 | x_rounded = r.round_object(x, use_copy=False) 747 | assert x_rounded is x 748 | 749 | 750 | def test_copy_for_MemberDescriptorType(): 751 | class Whatever: 752 | __slots__ = ("whatever",) 753 | 754 | x = Whatever.__dict__["whatever"] 755 | x_rounded = r.round_object(x, use_copy=True) 756 | assert x_rounded is x 757 | 758 | 759 | def test_no_copy_for_MemberDescriptorType(): 760 | class Whatever: 761 | __slots__ = ("whatever",) 762 | 763 | x = Whatever.__dict__["whatever"] 764 | x_rounded = r.round_object(x, use_copy=False) 765 | assert x_rounded is x 766 | 767 | 768 | def test_copy_for_BuiltinMethodType(): 769 | from datetime import date 770 | 771 | x = date.today 772 | x_rounded = r.round_object(x, use_copy=True) 773 | assert x_rounded is x 774 | 775 | 776 | def test_no_copy_for_BuiltinMethodType(): 777 | from datetime import date 778 | 779 | x = date.today 780 | x_rounded = r.round_object(x, use_copy=False) 781 | assert x_rounded is x 782 | 783 | 784 | def test_copy_for_MethodWrapperType(): 785 | class Whatever: 786 | pass 787 | 788 | x = Whatever.__eq__ 789 | x_rounded = r.round_object(x, use_copy=True) 790 | assert x_rounded is x 791 | 792 | 793 | def test_no_copy_for_MethodWrapperType(): 794 | class Whatever: 795 | pass 796 | 797 | x = Whatever.__eq__ 798 | x_rounded = r.round_object(x, use_copy=False) 799 | assert x_rounded is x 800 | 801 | 802 | def test_signif_edge_case(): 803 | assert r.signif(123123123123.0002, 7) == 123123100000 804 | assert r.signif_object(123123123123.0002, 7) == 123123100000 805 | 806 | 807 | def test_range(): 808 | rng = range(8, 12) 809 | assert list(r.signif_object(rng, 1)) == [8.0, 9.0, 10, 10] 810 | assert list(r.signif_object(rng, 1)) == [8.0, 9.0, 10, 10] 811 | 812 | assert list(r.signif_object(rng, 1, True)) == [8.0, 9.0, 10, 10] 813 | assert list(r.signif_object(rng, 1, True)) == [8.0, 9.0, 10, 10] 814 | 815 | 816 | def test_filter(): 817 | flt = filter(lambda x: x >= 8, range(12)) 818 | assert list(r.signif_object(flt, 1)) == [8.0, 9.0, 10, 10] 819 | assert list(r.signif_object(flt, 1)) == [] 820 | 821 | flt = filter(lambda x: x >= 8, range(12)) 822 | assert list(r.signif_object(flt, 1, True)) == [8.0, 9.0, 10, 10] 823 | assert list(r.signif_object(flt, 1)) == [8.0, 9.0, 10, 10] 824 | 825 | 826 | def test_generator(): 827 | gen = (x for x in range(8, 12)) 828 | assert list(r.signif_object(gen, 1)) == [8.0, 9.0, 10, 10] 829 | assert list(r.signif_object(gen, 1)) == [] 830 | 831 | gen = (x for x in range(8, 12)) 832 | assert list(r.signif_object(gen, 1, True)) == [ 833 | 8.0, 834 | 9.0, 835 | 10, 836 | 10, 837 | ] # for generators use_copy is ignored 838 | assert list(r.signif_object(gen, 1)) == [] 839 | 840 | 841 | def test_copy_for_map(): 842 | m = map(lambda i: i / 3, range(10)) 843 | m_rounded_copy = r.round_object(m, 1, use_copy=True) 844 | expected = [0.0, 0.3, 0.7, 1.0, 1.3, 1.7, 2.0, 2.3, 2.7, 3.0] 845 | assert list(m_rounded_copy) == expected 846 | assert len(list(m)) == 10 847 | 848 | 849 | def test_no_copy_for_map_without_copy(): 850 | m = map(lambda i: i / 3, range(10)) 851 | m_rounded_no_copy = r.round_object(m, 1, use_copy=False) 852 | expected = [0.0, 0.3, 0.7, 1.0, 1.3, 1.7, 2.0, 2.3, 2.7, 3.0] 853 | assert list(m_rounded_no_copy) == expected 854 | assert len(list(m)) == 0 855 | 856 | 857 | def test_copy_complex_object_with_map(): 858 | obj = { 859 | "list": [1.122, 55.55554, 234.3], 860 | "float": 1.11122156, 861 | "int": 1212, 862 | "dict": { 863 | "filter": filter(lambda i: i % 2 != 0, [1, 1.2323, 2, 22.22]), 864 | "map": map(lambda i: i / 3, range(10)), 865 | }, 866 | } 867 | obj_rounded = r.round_object(obj, 1, use_copy=True) 868 | # filter test changed ! 869 | assert list(obj_rounded["dict"]["filter"]) == [1, 1.2, 22.2] 870 | expected = [0.0, 0.3, 0.7, 1.0, 1.3, 1.7, 2.0, 2.3, 2.7, 3.0] 871 | assert list(obj_rounded["dict"]["map"]) == expected 872 | assert len(list(obj["dict"]["map"])) == 10 873 | 874 | 875 | def test_no_copy_complex_object_with_map(): 876 | obj = { 877 | "list": [1.122, 55.55554, 234.3], 878 | "float": 1.11122156, 879 | "int": 1212, 880 | "dict": { 881 | "filter": filter(lambda i: i % 2 != 0, [1, 1.2323, 2, 22.22]), 882 | "map": map(lambda i: i / 3, range(10)), 883 | }, 884 | } 885 | obj_rounded = r.round_object(obj, 1, use_copy=False) 886 | 887 | assert list(obj_rounded["dict"]["filter"]) == [1, 1.2, 22.2] 888 | expected = [0.0, 0.3, 0.7, 1.0, 1.3, 1.7, 2.0, 2.3, 2.7, 3.0] 889 | assert list(obj_rounded["dict"]["map"]) == expected 890 | assert len(list(obj["dict"]["map"])) == 0 891 | 892 | 893 | def test_int_from_signif(): 894 | assert isinstance(r.signif(12.2, 2), float) 895 | assert isinstance(r.signif(1222222, 3), int) 896 | 897 | x = [{"x": 2, "y": 45.556}, (175, 1222.3), 2] 898 | assert isinstance(x[0]["x"], int) 899 | assert isinstance(x[2], int) 900 | 901 | x_rounded = r.signif_object(x) 902 | assert isinstance(x_rounded[0]["x"], int) 903 | assert isinstance(x_rounded[2], int) 904 | 905 | 906 | def test_int_return(): 907 | assert isinstance(r.round_object(10), int) 908 | assert isinstance(r.ceil_object(10.234234), int) 909 | assert isinstance(r.floor_object(10.234234), int) 910 | --------------------------------------------------------------------------------