#!/usr/bin/env python
# -*- coding: utf-8 -*-
# =============================================================================
# Licensed under a 3-clause BSD style license, see LICENSE.
# =============================================================================
"""
Set of utilities for comparison of floating point numbers inspired by
- https://randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition/
and the previous version:
- http://www.cygnus-software.com/papers/comparingfloats/Obsolete%20comparing%20floating%20point%20numbers.htm
"""

__all__ = (
    "distance",  ## distance in ULPs
    "isclose",  ## close in  terms of ULPs ?
    "isequal",  ## isequal using  ULPs and SCALE
    "next_double",  ## get next double
)


def distance(a, b):
    """
    Distance in ULPS between two (floating point) numbers.
    It is assumed here that  size(long)==size(double) for underlying C-library!

    Example
    -------
    >>> a = ...
    >>> b = ...
    >>> print distance ( a , b )
    """
    if a == b:
        return 0
    elif a > b:
        return -distance(b, a)
    elif b <= 0:
        return distance(-b, -a)
    elif a < 0:
        return distance(0, -a) + distance(0, b)

    ## here a and b have same sign

    import ctypes

    a, b = abs(a), abs(b)

    aa = ctypes.c_double(float(a))
    bb = ctypes.c_double(float(b))

    al = ctypes.c_long.from_buffer(aa).value
    bl = ctypes.c_long.from_buffer(bb).value

    return bl - al


def next_double(a, ulps=1):
    """
    Get the ``next-double'' by certain ULPs distance.
    It is assumed here that size(long)==size(double) for underlying C-library!

    Examples
    --------
    >>> a = next_double ( 1 , 1 )
    >>> print a , a - sys.float_info.epsilon
    """

    a = float(a)
    if 0 == ulps:
        return a
    elif a < 0:
        return -next_double(-a, -ulps)
    elif 0 > ulps:
        d = distance(a, 0.0) + ulps
        if d < 0:
            return -next_double(0.0, -d)

    ## aply a trick  with casting

    import ctypes

    aa = ctypes.c_double(a)
    al = ctypes.c_long.from_buffer(aa).value
    al = ctypes.c_long(al + ulps)
    aa = ctypes.c_double.from_buffer(al).value

    return aa


def isclose(a, b, ulps=1000):
    """
    Are two floating point numbers close enough (in units is ULPs)?

    Examples
    --------
    >>> a = ...
    >>> b = ...
    >>> print isclose ( a , b , 1000 )
    """
    return (a == b) or ulps >= abs(distance(a, b))


def isequal(a, b, scale=1.0, absdiff=0.0, ulps=1000):
    """
    Are  two numbers ``a'' and ``b''  close enough ?
    Numbers are considered to be equal is :
    - (for absdiff >0) the absolute difference is smaller than absdiff
    - *OR* they are close enough in ULPs
    - *OR* (for scale != 0) the difference is small enough compared to the scale

    Examples
    --------
    >>> isequal(1,1+sys.float_info.epsilon)
    True
    >>> isequal(1,1+sys.float_info.epsilon, ulps = 0 )
    False
    >>> isequal(1,1+sys.float_info.epsilon, scale = 100 , ulps = 0 )
    True
    """
    return (
        (a == b)
        or (0 < absdiff and abs(a - b) < absdiff)
        or isclose(a, b, ulps)
        or (scale and isclose((a - b) + scale, scale, ulps))
    )
