Repository: pyccel/pyccel
Pull request #996 (2021-10-15 20:56:21)

Issue #983 | Add Round function and implement the conversion to C, Fortran, and Python
Add support for Python's `round` function which is implemented with a banker's round. An integer and a floating point implementation are both required to avoid floating point errors when handling integers. Fixes #983.

A minor bug fix is also included to ensure that the error mode is set back to 'user' when nothing is specified if the developer mode was used previously.

EmilyBourne: Hi, how are you doing? Are you still working on this PR? Do you need a hand? I just wanted to remind you to keep your branch up to date with the master branch. There have been a few aesthetic changes recently (mainly PR #1012) which should be merged sooner rather than later to avoid accumulating conflicts
EmilyBourne: `round` in python has a very strange way of handling values that are equally close to 2 integers (see [here](https://docs.python.org/3/library/functions.html#round)):

> For the built-in types supporting [round()](https://docs.python.org/3/library/functions.html#round), values are rounded to the closest multiple of 10 to the power minus ndigits; if two multiples are equally close, rounding is done toward the even choice (so, for example, both round(0.5) and round(-0.5) are 0, and round(1.5) is 2). Any integer value is valid for ndigits (positive, zero, or negative). The return value is an integer if ndigits is omitted or None. Otherwise, the return value has the same type as number.

This needs to be handled, maybe we need to implement our own `round` function in `pyc_math` for [c](https://github.com/pyccel/pyccel/blob/master/pyccel/stdlib/math/pyc_math_c.h) and [fortran](https://github.com/pyccel/pyccel/blob/master/pyccel/stdlib/math/pyc_math_f90.f90)
aihya: An Error is raised when I tried the following code:
```python3
if __name__ == "__main__":
    print(round(42, ndigits=5))
```

The error:
```
ERROR at shared library generation stage
Traceback (most recent call last):
  File "/home/glitch/Documents/nandiniraja348/nandiniraja348_env/bin/pyccel", line 33, in <module>
    sys.exit(load_entry_point('pyccel', 'console_scripts', 'pyccel')())
  File "/home/glitch/Documents/nandiniraja348/pyccel/commands/console.py", line 241, in pyccel
    execute_pyccel(filename,
  File "/home/glitch/Documents/nandiniraja348/pyccel/codegen/pipeline.py", line 382, in execute_pyccel
    generated_program_filepath = src_compiler.compile_program(compile_obj=prog_obj,
  File "/home/glitch/Documents/nandiniraja348/pyccel/codegen/compiling/compilers.py", line 345, in compile_program
    self.run_command(cmd, verbose)
  File "/home/glitch/Documents/nandiniraja348/pyccel/codegen/compiling/compilers.py", line 429, in run_command
    raise RuntimeError(err_msg)
RuntimeError: Failed to build module
/home/glitch/Documents/nandiniraja348/__pyccel__/prog_test_round.f90:9:36:

    9 |   write(*, '(F0.12)', advance="yes") pyc_bankers_round(42_i64, 5_i64)
      |                                    1
Error: There is no specific function for the generic ‘pyc_bankers_round’ at (1)
```
yguclu: @aihya Good catch! This is because the two functions implemented in Fortran require the first argument to be a float (single or double precision), while here you are passing an integer.

cc: @nandiniraja348 @EmilyBourne 
aihya: Incorrect rounded value when passing non-integer to `ndigits`:
```python
if __name__ == "__main__":
    print(round(42, ndigits=4.2))
```
Output: `41.999991113996`

```python
if __name__ == "__main__":
    print(round(42, ndigits=4.2))
```
Output: `4.200030659267`

EmilyBourne: @aihya 
Pyccel doesn't translate code which doesn't run in python. Your example:
> ```python
> if __name__ == "__main__":
>     print(round(4.2, ndigits=4.2))
> ```
gives:
```python
TypeError                                 Traceback (most recent call last)
<ipython-input-1-dd8329dcb3e7> in <module>
----> 1 print(round(4.2, ndigits=4.2))

TypeError: 'float' object cannot be interpreted as an integer
```
EmilyBourne: I've just fixed the integer case. I am going to try and rebase this branch to see if we can get this merged
EmilyBourne: /bot checklist
EmilyBourne: /bot run pylint pyccel_lint spelling docs
EmilyBourne: /bot run pr_tests
EmilyBourne: /bot run pr_tests
EmilyBourne: /bot run pr_tests
EmilyBourne: /bot run linux
EmilyBourne: /bot run windows
EmilyBourne: /bot run windows 
EmilyBourne: /bot run windows 
EmilyBourne: /bot run windows 
EmilyBourne: /bot run windows
EmilyBourne: /bot run pr_tests
EmilyBourne: /bot run windows 
EmilyBourne: /bot run pr_tests
EmilyBourne: > I think the round functions implemented, though sound, introduce errors due to floating point arithmetic:
> 
> ```python
> if __name__ == '__main__':
>     print(round(2.675, 2))
> ```
> 
> this gives 2.68 instead of 2.67

This is a very surprising corner case. To me this looks like a Python rounding error rather than a C rounding error. Python implement's banker's round:
> Bankers' rounding takes its name from a history of being used in banks. It rounds x.5 to nearest even number. The reason is to evenly spread rounding of x.5 up and down. This increases the likelihood that the sum of many rounded numbers is closer to the true sum.

So I would expect `2.675` to round to `2.68`. However in Python I have:
```python
In [18]: print(f"{2.675:.16f}")
2.6749999999999998
```
0.49999999999998 therefore rounds down.

It seems that the correction comes from the multiplication by 100:
```python
In [20]: print(f"{2.675*100.0:.16f}")
267.5000000000000000
```
I can fix this. But I wonder if I should given that the C answer is more accurate :thinking: 
EmilyBourne: Copy/pasting Python's `round` code from:
https://github.com/python/cpython/blob/0f56adb8d74e703974811620559d96c999334547/Objects/floatobject.c#L985
leads to the same results as our current implementation.

I suspect that the result with the rounding comes from this implementation:
https://github.com/python/cpython/blob/0f56adb8d74e703974811620559d96c999334547/Objects/floatobject.c#L927
which relies on printing floats to strings to truncate. Converting a float to a string, truncating and then converting back to a float would not be good for performance so I would rather keep the current implementation. This will give the same result in most cases. In some cases (such as `2.675, 2`) it may be closer to the expected result, there will probably be other cases where this method suffers from rounding errors instead but on balance I think this is no worse than Python's implementation.
EmilyBourne: /bot run pr_tests

Patch:
diff --git a/AUTHORS b/AUTHORS
index 18f3dc09..72908bd9 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -38,4 +38,5 @@ Contributors
 * Mohammed Ayaz Shaikh
 * Amir Movassaghi
 * Leonardo Merlin Paloschi
-* Joao Antonio Gomes
\ No newline at end of file
+* Joao Antonio Gomes
+* Nandini Raja
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0c0bb6bd..bf07304f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -61,6 +61,7 @@ All notable changes to this project will be documented in this file.
 -   Add support for inhomogeneous tuple annotations.
 -   #1834 : Add support for `@property` decorator.
 -   #2099 : Fix translation of modules containing `__all__`.
+-   #983 : Add support for built-in function `round`.
 -   \[INTERNALS\] Add abstract class `SetMethod` to handle calls to various set methods.
 -   \[INTERNALS\] Added `container_rank` property to `ast.datatypes.PyccelType` objects.
 -   \[INTERNALS\] Add a `__call__` method to `FunctionDef` to create `FunctionCall` instances.
diff --git a/docs/builtin-functions.md b/docs/builtin-functions.md
index bff5b94e..540319f3 100644
--- a/docs/builtin-functions.md
+++ b/docs/builtin-functions.md
@@ -59,7 +59,7 @@ Python contains a limited number of builtin functions defined [here](https://doc
 | **`range`** | **Yes** |
 | `repr` | No |
 | `reversed` | No |
-| `round` | No |
+| **`round`** | **Yes** |
 | *`set`* | Python-only |
 | `setattr` | No  |
 | `slice` | No |
diff --git a/pyccel/ast/builtins.py b/pyccel/ast/builtins.py
index 75c33192..45b89ef6 100644
--- a/pyccel/ast/builtins.py
+++ b/pyccel/ast/builtins.py
@@ -16,7 +16,8 @@ from pyccel.utilities.stage import PyccelStage
 
 from .basic     import PyccelAstNode, TypedAstNode
 from .datatypes import PythonNativeInt, PythonNativeBool, PythonNativeFloat
-from .datatypes import GenericType, PythonNativeComplex, PrimitiveComplexType
+from .datatypes import GenericType, PythonNativeComplex
+from .datatypes import PrimitiveBooleanType, PrimitiveComplexType
 from .datatypes import HomogeneousTupleType, InhomogeneousTupleType
 from .datatypes import HomogeneousListType, HomogeneousContainerType
 from .datatypes import FixedSizeNumericType, HomogeneousSetType, SymbolicType
@@ -53,6 +54,7 @@ __all__ = (
     'PythonPrint',
     'PythonRange',
     'PythonReal',
+    'PythonRound',
     'PythonSet',
     'PythonSetFunction',
     'PythonSum',
@@ -480,6 +482,52 @@ class PythonFloat(PyccelFunction):
     def __str__(self):
         return f'float({self.arg})'
 
+# ===========================================================================
+class PythonRound(PyccelFunction):
+    """
+    Class representing a call to Python's native round() function.
+
+    Class representing a call to Python's native round() function
+    which rounds a float or integer to a given number of decimals.
+
+    Parameters
+    ----------
+    number : TypedAstNode
+        The number to be rounded.
+    ndigits : TypedAstNode, optional
+        The number of digits to round to.
+    """
+    __slots__ = ('_class_type',)
+    name = 'round'
+    _rank = 0
+    _shape = None
+    _order = None
+
+    def __init__(self, number, ndigits = None):
+        if ndigits is None or number.class_type.primitive_type is PrimitiveBooleanType():
+            self._class_type = PythonNativeInt()
+        else:
+            self._class_type = number.class_type
+        super().__init__(number, ndigits)
+
+    @property
+    def arg(self):
+        """
+        The number to be rounded.
+
+        The number to be rounded.
+        """
+        return self._args[0]
+
+    @property
+    def ndigits(self):
+        """
+        The number of digits to which the argument is rounded.
+
+        The number of digits to which the argument is rounded.
+        """
+        return self._args[1]
+
 #==============================================================================
 class PythonInt(PyccelFunction):
     """
@@ -1664,6 +1712,7 @@ builtin_functions_dict = {
     'min'      : PythonMin,
     'not'      : PyccelNot,
     'range'    : PythonRange,
+    'round'    : PythonRound,
     'set'      : PythonSetFunction,
     'str'      : LiteralString,
     'sum'      : PythonSum,
diff --git a/pyccel/codegen/printing/ccode.py b/pyccel/codegen/printing/ccode.py
index 308b3a1b..fe43e9b5 100644
--- a/pyccel/codegen/printing/ccode.py
+++ b/pyccel/codegen/printing/ccode.py
@@ -749,6 +749,15 @@ class CCodePrinter(CodePrinter):
             func = "labs"
         return "{}({})".format(func, self._print(expr.arg))
 
+    def _print_PythonRound(self, expr):
+        self.add_import(c_imports['pyc_math_c'])
+        arg = self._print(expr.arg)
+        ndigits = self._print(expr.ndigits or LiteralInteger(0))
+        if isinstance(expr.arg.class_type.primitive_type, (PrimitiveBooleanType, PrimitiveIntegerType)):
+            return f'ipyc_bankers_round({arg}, {ndigits})'
+        else:
+            return f'fpyc_bankers_round({arg}, {ndigits})'
+
     def _print_PythonMinMax(self, expr):
         arg = expr.args[0]
         if arg.dtype.primitive_type is PrimitiveFloatingPointType() and len(arg) == 2:
diff --git a/pyccel/codegen/printing/fcode.py b/pyccel/codegen/printing/fcode.py
index 09ecbbcc..4777ebd7 100644
--- a/pyccel/codegen/printing/fcode.py
+++ b/pyccel/codegen/printing/fcode.py
@@ -71,7 +71,7 @@ from pyccel.ast.numpyext import NumpyNonZero
 from pyccel.ast.numpyext import NumpySign
 from pyccel.ast.numpyext import NumpyIsFinite, NumpyIsNan
 
-from pyccel.ast.numpytypes import NumpyNDArrayType
+from pyccel.ast.numpytypes import NumpyNDArrayType, NumpyInt64Type
 
 from pyccel.ast.operators import PyccelAdd, PyccelMul, PyccelMinus, PyccelAnd, PyccelEq
 from pyccel.ast.operators import PyccelMod, PyccelNot, PyccelAssociativeParenthesis
@@ -1198,6 +1198,25 @@ class FCodePrinter(CodePrinter):
         arg_code = self._get_node_without_gFTL(expr.arg)
         return f"abs({arg_code})"
 
+    def _print_PythonRound(self, expr):
+        arg = expr.arg
+        if not isinstance(arg.dtype.primitive_type, PrimitiveFloatingPointType):
+            arg = self._apply_cast(NumpyInt64Type(), arg)
+        self.add_import(Import('pyc_math_f90', Module('pyc_math_f90',(),())))
+
+        arg_code = self._print(arg)
+        ndigits = self._apply_cast(NumpyInt64Type(), expr.ndigits) if expr.ndigits \
+                else LiteralInteger(0, NumpyInt64Type())
+        ndigits_code = self._print(ndigits)
+
+        code = f'pyc_bankers_round({arg_code}, {ndigits_code})'
+
+        if not isinstance(expr.dtype.primitive_type, PrimitiveFloatingPointType):
+            prec = self.print_kind(expr)
+            return f"Int({code}, kind={prec})"
+        else:
+            return code
+
     def _print_PythonTuple(self, expr):
         shape = tuple(reversed(expr.shape))
         if len(shape)>1:
diff --git a/pyccel/codegen/printing/pycode.py b/pyccel/codegen/printing/pycode.py
index e22e8007..0a74d30c 100644
--- a/pyccel/codegen/printing/pycode.py
+++ b/pyccel/codegen/printing/pycode.py
@@ -507,6 +507,14 @@ class PythonCodePrinter(CodePrinter):
         else:
             raise NotImplementedError("The shape access function seems to be poorly defined.")
 
+    def _print_PythonRound(self, expr):
+        arg = self._print(expr.arg)
+        if expr.ndigits:
+            ndigits = self._print(expr.ndigits)
+            return f'round({arg}, {ndigits})'
+        else:
+            return f'round({arg})'
+
     def _print_PyccelArraySize(self, expr):
         arg = self._print(expr.arg)
         name = self._get_numpy_name(expr)
diff --git a/pyccel/commands/console.py b/pyccel/commands/console.py
index 596c440e..4e76fbd3 100644
--- a/pyccel/commands/console.py
+++ b/pyccel/commands/console.py
@@ -248,11 +248,13 @@ def pyccel(files=None, mpi=None, openmp=None, openacc=None, output_dir=None, com
     # ...
 
     # ...
+    # this will initialize the singelton ErrorsMode
+    # making this settings available everywhere
+    err_mode = ErrorsMode()
     if args.developer_mode:
-        # this will initialize the singelton ErrorsMode
-        # making this settings available everywhere
-        err_mode = ErrorsMode()
         err_mode.set_mode('developer')
+    else:
+        err_mode.set_mode('user')
     # ...
 
     base_dirpath = os.getcwd()
diff --git a/pyccel/commands/epyccel.py b/pyccel/commands/epyccel.py
index da39dfd6..7f8d8c1f 100644
--- a/pyccel/commands/epyccel.py
+++ b/pyccel/commands/epyccel.py
@@ -330,11 +330,13 @@ def epyccel( python_function_or_module, **kwargs ):
     comm  = kwargs.pop('comm', None)
     root  = kwargs.pop('root', 0)
     bcast = kwargs.pop('bcast', True)
+    # This will initialize the singleton ErrorsMode
+    # making this setting available everywhere
+    err_mode = ErrorsMode()
     if kwargs.pop('developer_mode', None):
-        # This will initialize the singleton ErrorsMode
-        # making this setting available everywhere
-        err_mode = ErrorsMode()
         err_mode.set_mode('developer')
+    else:
+        err_mode.set_mode('user')
 
     # Parallel version
     if comm is not None:
diff --git a/pyccel/stdlib/math/pyc_math_c.c b/pyccel/stdlib/math/pyc_math_c.c
index 585d7ee8..1b608ab6 100644
--- a/pyccel/stdlib/math/pyc_math_c.c
+++ b/pyccel/stdlib/math/pyc_math_c.c
@@ -39,13 +39,6 @@ int64_t                     pyc_lcm (int64_t a, int64_t b)
     return a / pyc_gcd(a, b) * b;
 }
 /*---------------------------------------------------------------------------*/
-extern inline double        pyc_radians(double degrees);
-/*---------------------------------------------------------------------------*/
-extern inline double        pyc_degrees(double radians);
-/*---------------------------------------------------------------------------*/
-extern inline int64_t       pyc_modulo(int64_t a, int64_t b);
-/*---------------------------------------------------------------------------*/
-extern inline double        pyc_fmodulo(double a, double b);
 
 /* numpy.sign for float, double and integers */
 long long int isign(long long int x)
@@ -70,3 +63,42 @@ double complex csign(double complex x)
     double absolute = cabs(x);
     return ((absolute == 0) ? 0.0 : (x / absolute));
 }
+
+/*---------------------------------------------------------------------------*/
+
+double fpyc_bankers_round(double arg, int64_t ndigits)
+{
+    double factor = pow(10.0, ndigits);
+    arg *= factor;
+
+    double nearest_int_fix = copysign(0.5, arg);
+
+    double rnd = (int64_t)(arg + nearest_int_fix);
+
+    double diff = arg - rnd;
+
+    if (ndigits <= 0 && (diff == 0.5 || diff == -0.5)) {
+        rnd = ((int64_t)(arg*0.5 + nearest_int_fix))*2.0;
+    }
+
+    return rnd / factor;
+}
+
+int64_t ipyc_bankers_round(int64_t arg, int64_t ndigits)
+{
+    if (ndigits >= 0) {
+        return arg;
+    } else {
+        int64_t mul_fact = 1;
+        for (int i = 0; i< -ndigits; ++i) mul_fact *= 10;
+
+        int64_t pivot_point = copysign(5*mul_fact/10, arg);
+        int64_t remainder = arg % mul_fact;
+        if ( remainder == pivot_point ) {
+            int64_t val = (mul_fact - remainder) / mul_fact;
+            return (val + (val & 1)) * mul_fact;
+        } else {
+            return ((arg + pivot_point) / mul_fact) * mul_fact;
+        }
+    }
+}
diff --git a/pyccel/stdlib/math/pyc_math_c.h b/pyccel/stdlib/math/pyc_math_c.h
index deb44d52..658484a2 100644
--- a/pyccel/stdlib/math/pyc_math_c.h
+++ b/pyccel/stdlib/math/pyc_math_c.h
@@ -46,4 +46,7 @@ double fsign(double x);
 double complex csgn(double complex x);
 double complex csign(double complex x);
 
+double fpyc_bankers_round(double arg, int64_t ndigits);
+int64_t ipyc_bankers_round(int64_t arg, int64_t ndigits);
+
 #endif
diff --git a/pyccel/stdlib/math/pyc_math_f90.f90 b/pyccel/stdlib/math/pyc_math_f90.f90
index 991428e7..7ea9496e 100644
--- a/pyccel/stdlib/math/pyc_math_f90.f90
+++ b/pyccel/stdlib/math/pyc_math_f90.f90
@@ -51,6 +51,22 @@ interface csign
     module procedure numpy_v2_sign_c64
 end interface csign
 
+interface pyc_bankers_round
+    module procedure pyc_bankers_round_float
+    module procedure pyc_bankers_round_int
+end interface pyc_bankers_round
+
+public :: pyc_gcd, &
+          pyc_factorial, &
+          pyc_lcm, &
+          pyc_radians, &
+          pyc_degrees, &
+          amax, &
+          amin, &
+          csgn, &
+          csign, &
+          pyc_bankers_round
+
 contains
 
 ! Implementation of math factorial function
@@ -351,4 +367,58 @@ function amin_4(arr) result(min_value)
     end if
 
   end function numpy_v2_sign_c64
+
+pure function pyc_bankers_round_float(arg, ndigits) result(rnd)
+
+    implicit none
+
+    real(f64), value     :: arg
+    integer(i64), value :: ndigits
+    real(f64)            :: rnd
+
+    real(f64) :: diff
+
+    arg = arg * 10._f64**ndigits
+
+    rnd = nint(arg, kind=i64)
+
+    diff = arg - rnd
+
+    if (ndigits <= 0 .and. (diff == 0.5_f64 .or. diff == -0.5_f64)) then
+        rnd = nint(arg*0.5_f64, kind=i64)*2_i64
+    end if
+
+    rnd = rnd * 10._f64**(-ndigits)
+
+end function pyc_bankers_round_float
+
+pure function pyc_bankers_round_int(arg, ndigits) result(rnd)
+
+    implicit none
+
+    integer(i64), value :: arg
+    integer(i64), value :: ndigits
+    integer(i64)        :: rnd
+
+    integer(i64) :: val
+    integer(i64) :: mul_fact
+    integer(i64) :: pivot_point
+    integer(i64) :: remainder
+
+    if (ndigits >= 0) then
+        rnd = arg
+    else
+        mul_fact = 10_i64**(-ndigits)
+        pivot_point = sign(5_i64*10_i64**(-ndigits-1_i64), arg)
+        remainder = modulo(arg, mul_fact)
+        if ( remainder == pivot_point ) then
+            val = (mul_fact - remainder) / mul_fact
+            rnd = (val + IAND(val, 1_i64)) * mul_fact
+        else
+            rnd = ((arg + pivot_point) / mul_fact) * mul_fact
+        endif
+    endif
+
+end function pyc_bankers_round_int
+
 end module pyc_math_f90


Test patch:
diff --git a/tests/epyccel/test_builtins.py b/tests/epyccel/test_builtins.py
index 0245160e..bb861d1a 100644
--- a/tests/epyccel/test_builtins.py
+++ b/tests/epyccel/test_builtins.py
@@ -478,3 +478,185 @@ def test_len_literal_string(language):
     epyc_f = epyccel(f, language = language)
 
     assert epyc_f() == f()
+
+def test_round_int(language):
+    def round_int(x : float):
+        return round(x)
+
+    f = epyccel(round_int, language=language)
+    x = randint(100) / 10
+
+    f_output = f(x)
+    round_int_output = round_int(x)
+    assert round_int_output == f_output
+    assert isinstance(f_output, type(round_int_output))
+
+    # Round down
+    x = 3.345
+
+    f_output = f(x)
+    round_int_output = round_int(x)
+    assert round_int_output == f_output
+    assert isinstance(f_output, type(round_int_output))
+
+    # Round up
+    x = 3.845
+
+    f_output = f(x)
+    round_int_output = round_int(x)
+    assert round_int_output == f_output
+    assert isinstance(f_output, type(round_int_output))
+
+    # Round half
+    x = 6.5
+
+    f_output = f(x)
+    round_int_output = round_int(x)
+    assert round_int_output == f_output
+    assert isinstance(f_output, type(round_int_output))
+
+def test_negative_round_int(language):
+    def round_int(x : float):
+        return round(x)
+
+    f = epyccel(round_int, language=language)
+    x = -randint(100) / 10
+
+    f_output = f(x)
+    round_int_output = round_int(x)
+    assert round_int_output == f_output
+    assert isinstance(f_output, type(round_int_output))
+
+    # Round up
+    x = -3.345
+
+    f_output = f(x)
+    round_int_output = round_int(x)
+    assert round_int_output == f_output
+    assert isinstance(f_output, type(round_int_output))
+
+    # Round down
+    x = -3.845
+
+    f_output = f(x)
+    round_int_output = round_int(x)
+    assert round_int_output == f_output
+    assert isinstance(f_output, type(round_int_output))
+
+    # Round half
+    x = -6.5
+
+    f_output = f(x)
+    round_int_output = round_int(x)
+    assert round_int_output == f_output
+    assert isinstance(f_output, type(round_int_output))
+
+def test_round_ndigits(language):
+    def round_ndigits(x : float, i : int):
+        return round(x,i)
+
+    f = epyccel(round_ndigits, language=language)
+    x = randint(100) / 10
+
+    f_output = f(x, 1)
+    round_ndigits_output = round_ndigits(x, 1)
+    assert np.isclose(round_ndigits_output, f_output)
+    assert isinstance(f_output, type(round_ndigits_output))
+
+    x = 3.343
+
+    f_output = f(x,2)
+    round_ndigits_output = round_ndigits(x,2)
+    assert np.isclose(round_ndigits_output, f_output)
+    assert isinstance(f_output, type(round_ndigits_output))
+
+    x = 3323.0
+
+    f_output = f(x,-2)
+    round_ndigits_output = round_ndigits(x, -2)
+    assert np.isclose(round_ndigits_output, f_output)
+    assert isinstance(f_output, type(round_ndigits_output))
+
+    x = -3390.0
+
+    f_output = f(x,-2)
+    round_ndigits_output = round_ndigits(x, -2)
+    assert np.isclose(round_ndigits_output, f_output)
+    assert isinstance(f_output, type(round_ndigits_output))
+
+def test_round_ndigits_half(language):
+    def round_ndigits(x : float, i : int):
+        return round(x,i)
+
+    f = epyccel(round_ndigits, language=language)
+    x = randint(100) / 10
+
+    f_output = f(x, 1)
+    round_ndigits_output = round_ndigits(x, 1)
+    assert np.isclose(round_ndigits_output, f_output)
+    assert isinstance(f_output, type(round_ndigits_output))
+
+    x = 3.345
+
+    f_output = f(x,2)
+    round_ndigits_output = round_ndigits(x,2)
+    assert np.isclose(round_ndigits_output, f_output)
+    assert isinstance(f_output, type(round_ndigits_output))
+
+    x = -3350.0
+
+    f_output = f(x,-2)
+    round_ndigits_output = round_ndigits(x, -2)
+    assert np.isclose(round_ndigits_output, f_output)
+    assert isinstance(f_output, type(round_ndigits_output))
+
+    x = 45.0
+
+    f_output = f(x,-1)
+    round_ndigits_output = round_ndigits(x,-1)
+    assert np.isclose(round_ndigits_output, f_output)
+    assert isinstance(f_output, type(round_ndigits_output))
+
+def test_round_ndigits_int(language):
+    def round_ndigits(x : int, i : int):
+        return round(x,i)
+
+    f = epyccel(round_ndigits, language=language)
+    x = randint(100) // 10
+
+    f_output = f(x, 1)
+    round_ndigits_output = round_ndigits(x, 1)
+    assert np.isclose(round_ndigits_output, f_output)
+    assert isinstance(f_output, type(round_ndigits_output))
+
+    x = 3
+
+    f_output = f(x,2)
+    round_ndigits_output = round_ndigits(x,2)
+    assert np.isclose(round_ndigits_output, f_output)
+    assert isinstance(f_output, type(round_ndigits_output))
+
+    x = 3323
+
+    f_output = f(x,-2)
+    round_ndigits_output = round_ndigits(x, -2)
+    assert np.isclose(round_ndigits_output, f_output)
+    assert isinstance(f_output, type(round_ndigits_output))
+
+    x = -3390
+
+    f_output = f(x,-2)
+    round_ndigits_output = round_ndigits(x, -2)
+    assert np.isclose(round_ndigits_output, f_output)
+    assert isinstance(f_output, type(round_ndigits_output))
+
+def test_round_ndigits_bool(language):
+    def round_ndigits():
+        return round(True), round(False), round(True, 1), round(True, -1)
+
+    f = epyccel(round_ndigits, language=language)
+
+    f_output = f()
+    round_ndigits_output = round_ndigits()
+    assert all(o == r for o, r in zip(f_output, round_ndigits_output))
+    assert all(isinstance(o, type(r)) for o, r in zip(f_output, round_ndigits_output))