Decimal for C++
От: Marty Пират https://www.youtube.com/channel/UChp5PpQ6T4-93HbNF-8vSYg
Дата: 27.12.20 14:58
Оценка:
Здравствуйте!

Неохота было что-то искать, накидал на скорую руку

#pragma once


#include <string>
#include <cstdint>
#include <utility>
#include <limits>
#include <iostream>
#include <exception>
#include <stdexcept>


//----------------------------------------------------------------------------
namespace marty
{



//----------------------------------------------------------------------------
// 4 294 967 295 uint32_t max
// 1 000 000 000 - must be enough for money or use more wide type in typedef below

typedef std::uint32_t  DecimalDenumeratorRawType;
typedef std::int32_t   DecimalDenumeratorSignedRawType;

//----------------------------------------------------------------------------
//  9 223 372 036 854 775 808
//  1 000 000 000.000 000  000 - 10**9 -  миллиард - бюджет страны верстать не выйдет с точностью 9 нулей после точки
//  1 000 000 000 000 00.0 000 - 10**15 - 100 трлн с точностью до сотых копейки - для домашней бухгалтерии норм

typedef std::int64_t   DecimalNumeratorRawType;



//----------------------------------------------------------------------------
class DecimalDenumerator;

class DecimalPrecision
{
    friend class DecimalDenumerator;

    std::uint32_t m_precision;

    // default constructor disabled
    DecimalPrecision( );
    //DecimalPrecision& operator=( const DecimalPrecision &dp );

public:

    typedef std::uint32_t                    precision_t;
    typedef DecimalDenumeratorRawType        denum_t;
    typedef DecimalDenumeratorSignedRawType  sdenum_t;

    static denum_t denum( precision_t pr )
    {
        if ( maxPrecision<denum_t>() < pr )
            throw std::runtime_error("DecimalPrecision::denum - taken precision to big");

        return (denum_t)(getPowers10Table()[pr]);

        /*
        denum_t  denum = 1;
        for(precision_t i=0; i!=pr; ++i)
        {
            denum_t next = denum * 10u;
            if (next<denum)
                throw std::runtime_error("DecimalPrecision precision to big");
            denum = next;
        }

        return denum;
        */
    }

    DecimalPrecision( std::uint32_t pr ) : m_precision(pr) {}
    DecimalPrecision( const DecimalPrecision &pr ) : m_precision(pr.m_precision) {}

    denum_t denum( ) const
    {
        return denum(m_precision);
    }

    precision_t prec() const 
    {
        return m_precision;
    }

protected:

    template<typename IntType>
    static precision_t maxPrecision()    { throw std::runtime_error("DecimalPrecision::maxPrecision not implemented"); }

    template<> static precision_t maxPrecision<std::int32_t >() { return  9; }
    template<> static precision_t maxPrecision<std::uint32_t>() { return  9; }
    template<> static precision_t maxPrecision<std::int64_t >() { return 18; }
    template<> static precision_t maxPrecision<std::uint64_t>() { return 18; }


    static std::uint64_t* getPowers10Table()
    {
        static std::uint64_t _[] = 
        { 1                             //  0
        , 10                            //  1
        , 100                           //  2
        , 1000                          //  3
        , 10000                         //  4
        , 100000                        //  5
        , 1000000                       //  6
        , 10000000                      //  7
        , 100000000                     //  8
        , 1000000000                    //  9 - max for int32_t
        , 10000000000                   // 10
        , 100000000000                  // 11
        , 1000000000000                 // 12
        , 10000000000000                // 13
        , 100000000000000               // 14
        , 1000000000000000              // 15
        , 10000000000000000             // 16
        , 100000000000000000            // 17
        , 1000000000000000000           // 18 - max for int64_t
        };
    
        return &_[0];
    }


}; // class DecimalPrecision

//----------------------------------------------------------------------------



#define MARTY_DECIMAL_IMPLEMENT_RELATIONAL_OPERATORS( typeT2 )\
                                                              \
    bool operator<( const typeT2 v2 ) const                   \
    {                                                         \
        return compare(v2)<0;                                 \
    }                                                         \
                                                              \
    bool operator<=( const typeT2 v2 ) const                  \
    {                                                         \
        return compare(v2)<=0;                                \
    }                                                         \
                                                              \
    bool operator>( const typeT2 v2 ) const                   \
    {                                                         \
        return compare(v2)>0;                                 \
    }                                                         \
                                                              \
    bool operator>=( const typeT2 v2 ) const                  \
    {                                                         \
        return compare(v2)>=0;                                \
    }                                                         \
                                                              \
    bool operator==( const typeT2 v2 ) const                  \
    {                                                         \
        return compare(v2)==0;                                \
    }                                                         \
                                                              \
    bool operator!=( const typeT2 v2 ) const                  \
    {                                                         \
        return compare(v2)!=0;                                \
    }





//----------------------------------------------------------------------------
class Decimal;

class DecimalDenumerator
{

public:

    typedef DecimalPrecision::denum_t       denum_t    ;
    typedef DecimalPrecision::sdenum_t      sdenum_t   ;
    typedef DecimalPrecision::precision_t   precision_t;

    friend class Decimal;

    DecimalDenumerator( const DecimalPrecision dp )
    : m_precision( dp.prec() )
    , m_denum    ( dp.denum() )
    {}

    DecimalDenumerator( const DecimalDenumerator &dd )
    : m_precision( dd.m_precision )
    , m_denum    ( dd.m_denum     )
    {}

    DecimalDenumerator& operator=( const DecimalDenumerator &dd )
    {
        m_precision = dd.m_precision;
        m_denum     = dd.m_denum    ;
        return *this;
    }

    precision_t prec () const { return m_precision; }
    denum_t     denum() const { return m_denum; }

    DecimalPrecision decimalPrecision( ) const { return DecimalPrecision(m_precision); }

    void swap( DecimalDenumerator &d2 )
    {
        //using namespace std;
        std::swap( m_precision, d2.m_precision );
        std::swap( m_denum    , d2.m_denum     );
    }

    int compare( const DecimalDenumerator d2 ) const
    {
        if (m_precision<d2.m_precision) return -1;
        if (m_precision>d2.m_precision) return  1;
        return 0;
    }

    MARTY_DECIMAL_IMPLEMENT_RELATIONAL_OPERATORS(DecimalDenumerator)


protected:

    void incPrec()
    {
        tryIncDenum();
        m_precision += 1;
    }

    void decPrec()
    {
        tryDecDenum();
        m_precision -= 1;
    }

    denum_t expandTo( precision_t p )
    {
        denum_t deltaDenum = 1;
        while(m_precision < p)
        {
            incPrec();
            deltaDenum *= 10;
        }

        return deltaDenum;
    }

    denum_t shrinkTo( precision_t p )
    {
        denum_t deltaDenum = 1;
        while(m_precision > p)
        {
            decPrec();
            deltaDenum *= 10;
        }

        return deltaDenum;
    }

    sdenum_t fitTo( precision_t p )
    {
        if (m_precision<p)
            return   (sdenum_t)expandTo(p);
        else
            return - (sdenum_t)shrinkTo(p);
    }


    void tryIncDenum()
    {
        denum_t newDenum = m_denum*10u;
        if (newDenum<m_denum)
            throw std::runtime_error("DecimalDenumerator precision to big to increment");
        m_denum = newDenum;
    }

    void tryDecDenum()
    {
        if (!m_denum)
            throw std::runtime_error("DecimalDenumerator precision to small to decrement");
        m_denum = m_denum / 10u;
    }


    // default constructor disabled
    DecimalDenumerator();

    precision_t  m_precision;
    denum_t      m_denum;

}; // class DecimalDenumerator

//----------------------------------------------------------------------------




//----------------------------------------------------------------------------
inline
void swap( DecimalDenumerator &d1, DecimalDenumerator &d2 )
{
    d1.swap( d2 );
}

//----------------------------------------------------------------------------




//----------------------------------------------------------------------------
std::string toString  ( const Decimal     &d );
Decimal     fromString( const std::string &d );
void swap( Decimal &d1, Decimal &d2 );

//----------------------------------------------------------------------------




//----------------------------------------------------------------------------
class Decimal
{

    friend std::string toString  ( const Decimal     &d );
    friend Decimal     fromString( const std::string &d );
    friend void swap( Decimal &d1, Decimal &d2 );


public:

    typedef DecimalNumeratorRawType           num_t      ;
    typedef DecimalDenumerator::denum_t       denum_t    ;
    typedef DecimalDenumerator::sdenum_t      sdenum_t   ;
    typedef DecimalDenumerator::precision_t   precision_t;

    typedef DecimalDenumerator                DenumeratorType;

    Decimal()                   : m_num(0)      , m_denum(DecimalPrecision(0))   {}
    Decimal( const Decimal &d ) : m_num(d.m_num), m_denum(d.m_denum)             {}
    Decimal( int            v, const DecimalPrecision &prec = DecimalPrecision(0) ) : m_num(v), m_denum(prec) { m_num *= m_denum.denum(); }
    Decimal( unsigned       v, const DecimalPrecision &prec = DecimalPrecision(0) ) : m_num(v), m_denum(prec) { m_num *= m_denum.denum(); }
    Decimal( std::int64_t   v, const DecimalPrecision &prec = DecimalPrecision(0) ) : m_num(v), m_denum(prec) { m_num *= m_denum.denum(); }
    Decimal( std::uint64_t  v, const DecimalPrecision &prec = DecimalPrecision(0) ) : m_num(v), m_denum(prec) { m_num *= m_denum.denum(); }

    Decimal( float          f, const DecimalPrecision &prec = DecimalPrecision(0) )
    : m_num(0), m_denum(prec)
    {
        fromFloat( f, prec );
    }

    Decimal( double          f, const DecimalPrecision &prec = DecimalPrecision(0) )
    : m_num(0), m_denum(prec)
    {
        fromFloat( f, prec );
    }

    void swap( Decimal &d2 )
    {
        //using namespace std;
        std::swap( m_num   , d2.m_num   );
        m_denum.swap( d2.m_denum );
    }


    Decimal& operator=( Decimal d2 )
    {
        swap(d2);
        return *this;
    }

    // операторы преобразования типа 

    explicit operator int() const
    {
        return (int)(m_num/m_denum.denum());
    }

    explicit operator unsigned() const
    {
        return (unsigned)(m_num/m_denum.denum());
    }

    explicit operator std::int64_t() const
    {
        return (m_num/m_denum.denum());
    }

    explicit operator std::uint64_t() const
    {
        return (std::uint64_t)(m_num/m_denum.denum());
    }

    explicit operator float() const
    {
        return (float)((double)m_num/(double)m_denum.denum());
    }

    explicit operator double() const
    {
        return ((double)m_num/(double)m_denum.denum());
    }


    Decimal operator + ( Decimal d2 ) const
    {
        Decimal d1 = *this;
        adjustPrecisions( d1, d2 );
        d1.m_num += d2.m_num;
        return d1;
    }

    Decimal operator - ( Decimal d2 ) const
    {
        Decimal d1 = *this;
        adjustPrecisions( d1, d2 );
        d1.m_num -= d2.m_num;
        return d1;
    }

    Decimal operator * ( Decimal d2 ) const
    {
        num_t num = m_num * d2.m_num;

        if (m_denum < d2.m_denum)
        {
            num /= m_denum.denum();
            return Decimal( num, d2.m_denum );
        }
        else
        {
            num /= d2.m_denum.denum();
            return Decimal( num, m_denum );
        }
    }

    // 1200.00000 / 22.000 = 54.5454545454545
    // 1200 00000 / 22 000 = 5454.54545454
    Decimal operator / ( Decimal d2 ) const
    {
        d2.minimizePrecision();

        Decimal d1 = *this;
        d1.expandTo( m_denum.prec() + d2.m_denum.prec() );
        return Decimal( d1.m_num / d2.m_num, m_denum);
    }

    // 1200.00000   % 22.000    = 12.000
    // 1200         % 22        = 12
    // 1200 00000   % 22 000    = 12000
    // 1200 000000  % 22 000    = 10000
    // 1200.00000   % 22.00000  = 12.00000
    // 120000000    % 2200000   = 12 00000

    Decimal operator % ( Decimal d2 ) const
    {
        Decimal d1 = *this;
        adjustPrecisions( d1, d2 );
        d1.m_num %= d2.m_num;
        return d1;
    }


    Decimal& operator += ( Decimal d2 )
    {
        *this = operator+(d2);
        return *this;
    }

    Decimal& operator -= ( Decimal d2 )
    {
        *this = operator-(d2);
        return *this;
    }

    Decimal& operator *= ( Decimal d2 )
    {
        *this = operator*(d2);
        return *this;
    }

    Decimal& operator /= ( Decimal d2 )
    {
        *this = operator/(d2);
        return *this;
    }

    Decimal& operator %= ( Decimal d2 )
    {
        *this = operator%(d2);
        return *this;
    }

    // Decimal operator +   ( int i ) const { return operator+ ( Decimal((int)i, DecimalPrecision(0)) ); }
    // Decimal& operator += ( int i )       { return operator+=( Decimal((int)i, DecimalPrecision(0)) ); }


    #define MARTY_DECIMAL_IMPLEMENT_ARIPHMETICT_OVERLOADS_FOR_INTEGRAL_TYPE( integralType )         \
                                                                                                    \
                Decimal operator +   ( integralType i ) const { return operator+ ( Decimal(i) ); }  \
                Decimal operator -   ( integralType i ) const { return operator- ( Decimal(i) ); }  \
                Decimal operator *   ( integralType i ) const { return operator* ( Decimal(i) ); }  \
                Decimal operator /   ( integralType i ) const { return operator/ ( Decimal(i) ); }  \
                Decimal operator %   ( integralType i ) const { return operator% ( Decimal(i) ); }  \
                                                                                                    \
                Decimal& operator += ( integralType i )       { return operator+=( Decimal(i) ); }  \
                Decimal& operator -= ( integralType i )       { return operator-=( Decimal(i) ); }  \
                Decimal& operator *= ( integralType i )       { return operator*=( Decimal(i) ); }  \
                Decimal& operator /= ( integralType i )       { return operator/=( Decimal(i) ); }  \
                Decimal& operator %= ( integralType i )       { return operator%=( Decimal(i) ); }


    MARTY_DECIMAL_IMPLEMENT_ARIPHMETICT_OVERLOADS_FOR_INTEGRAL_TYPE( int           )
    MARTY_DECIMAL_IMPLEMENT_ARIPHMETICT_OVERLOADS_FOR_INTEGRAL_TYPE( unsigned      )
    MARTY_DECIMAL_IMPLEMENT_ARIPHMETICT_OVERLOADS_FOR_INTEGRAL_TYPE( std::int64_t  )
    MARTY_DECIMAL_IMPLEMENT_ARIPHMETICT_OVERLOADS_FOR_INTEGRAL_TYPE( std::uint64_t )
    MARTY_DECIMAL_IMPLEMENT_ARIPHMETICT_OVERLOADS_FOR_INTEGRAL_TYPE( float         )
    MARTY_DECIMAL_IMPLEMENT_ARIPHMETICT_OVERLOADS_FOR_INTEGRAL_TYPE( double        )



    int compare( Decimal d2 ) const
    {
        if (m_denum.prec()==d2.m_denum.prec())
        {
            return compare(d2.m_num);
        }

        Decimal d1 = *this;
        adjustPrecisions( d1, d2 );
        return d1.compare(d2);
    }

    MARTY_DECIMAL_IMPLEMENT_RELATIONAL_OPERATORS(Decimal)


    static
    Decimal fromRawNumPrec( num_t num, precision_t prec )
    {
        Decimal res;

        res.m_num   = num;
        res.m_denum = DecimalPrecision(prec);

        return res;
    }


protected:


    Decimal( num_t n, DenumeratorType denum) : m_num(n), m_denum(denum)   {}

    static
    void adjustPrecisions( Decimal &d1, Decimal &d2 )
    {
        int cmp = d1.m_denum.compare( d2.m_denum );
        if (cmp==0) return;

        if (cmp<0)
           d1.expandTo(d2.m_denum.prec());
        else
           d2.expandTo(d1.m_denum.prec());
    }

    std::string rtrimZeros( std::string s )
    {
        while( !s.empty() && s.back()=='0' ) s.erase(s.size()-1, 1);
        if ( !s.empty() && (s.back()=='.' || s.back()==',') )
            s.append(1,'0');
        return s;
    }

    template<typename FloatType>
    void fromFloat( FloatType f, precision_t p )
    {
        bool precAuto = (p==0);
        *this = fromString(rtrimZeros(std::to_string(f)));
        if (!precAuto)
            fitTo(p);
    }

    template<typename FloatType>
    void fromFloat( FloatType f, DecimalPrecision p )
    {
        fromFloat( f, p.prec() );
    }

    int compare( num_t n ) const
    {
        if (m_num<n) return -1;
        if (m_num>n) return  1;
        return 0;
    }

    void expandTo( precision_t p )
    {
        denum_t adjust = m_denum.expandTo(p);
        m_num *= adjust;
    }

    void shrinkTo( precision_t p )
    {
        denum_t adjust = m_denum.shrinkTo(p);
        m_num /= adjust;
    }

    void fitTo( precision_t p )
    {
        sdenum_t adjust = m_denum.fitTo(p);
        if (adjust<0)
           m_num /= (denum_t)-adjust;
        else
           m_num *= (denum_t)adjust;
    }

    void minimizePrecision()
    {
        while( ((m_num%10)==0) && (m_denum.prec()>0) )
        {
            m_num /= 10;
            m_denum.decPrec();
        }
    }


    
    //precision_t

    //DecimalPrecision decimalPrecision( )

    num_t               m_num;
    DenumeratorType     m_denum;

}; // class Decimal

//----------------------------------------------------------------------------





//----------------------------------------------------------------------------
#define MARTY_DECIMAL_IMPLEMENT_FRIEND_OVERLOADS_FOR_INTEGRAL_TYPE( integralType )                                          \
                                                                                                                            \
    inline bool operator< ( integralType i, const Decimal &d ) { return Decimal(i) <  d; }                                  \
    inline bool operator<=( integralType i, const Decimal &d ) { return Decimal(i) <= d; }                                  \
    inline bool operator> ( integralType i, const Decimal &d ) { return Decimal(i) >  d; }                                  \
    inline bool operator>=( integralType i, const Decimal &d ) { return Decimal(i) >= d; }                                  \
    inline bool operator==( integralType i, const Decimal &d ) { return Decimal(i) == d; }                                  \
    inline bool operator!=( integralType i, const Decimal &d ) { return Decimal(i) != d; }                                  \
                                                                                                                            \
    inline Decimal operator + ( integralType i, const Decimal &d ) { return Decimal(i).operator+ ( d ); }                   \
    inline Decimal operator - ( integralType i, const Decimal &d ) { return Decimal(i).operator- ( d ); }                   \
    inline Decimal operator * ( integralType i, const Decimal &d ) { return Decimal(i).operator* ( d ); }                   \
    inline Decimal operator / ( integralType i, const Decimal &d ) { return Decimal(i).operator/ ( d ); }                   \
    inline Decimal operator % ( integralType i, const Decimal &d ) { return Decimal(i).operator% ( d ); }

//----------------------------------------------------------------------------




//----------------------------------------------------------------------------
MARTY_DECIMAL_IMPLEMENT_FRIEND_OVERLOADS_FOR_INTEGRAL_TYPE( int           )
MARTY_DECIMAL_IMPLEMENT_FRIEND_OVERLOADS_FOR_INTEGRAL_TYPE( unsigned      )
MARTY_DECIMAL_IMPLEMENT_FRIEND_OVERLOADS_FOR_INTEGRAL_TYPE( std::int64_t  )
MARTY_DECIMAL_IMPLEMENT_FRIEND_OVERLOADS_FOR_INTEGRAL_TYPE( std::uint64_t )
MARTY_DECIMAL_IMPLEMENT_FRIEND_OVERLOADS_FOR_INTEGRAL_TYPE( float         )
MARTY_DECIMAL_IMPLEMENT_FRIEND_OVERLOADS_FOR_INTEGRAL_TYPE( double        )

//----------------------------------------------------------------------------




//----------------------------------------------------------------------------
inline
void swap( Decimal &d1, Decimal &d2 )
{
    d1.swap(d2);
}

//----------------------------------------------------------------------------
inline
std::string toString  ( const Decimal     &d )
{
    Decimal::num_t p1 = d.m_num / d.m_denum.denum();
    Decimal::num_t p2 = d.m_num % d.m_denum.denum();

    std::string res = std::to_string(p1);

    std::size_t prec = d.m_denum.prec();

    if (!prec)
        return res; // No decimal digits after dot

    if (p2<0)
        p2 = -p2;

    std::string strP2 = std::to_string(p2);

    std::size_t leadingZerosNum = 0;
    if (prec>strP2.size())
        leadingZerosNum = prec - strP2.size();

    return res + std::string(1, '.') + std::string(leadingZerosNum, '0') + strP2;
}

//----------------------------------------------------------------------------
template<typename CharType>
inline
int charToDecimalDigit( CharType ch )
{
    if (ch<(CharType)'0') return -1;
    if (ch>(CharType)'9') return -1;
    return (int)(ch - (CharType)'0');
}

//----------------------------------------------------------------------------
template<typename CharType>
inline
bool charIsDecimalSeparator( CharType ch )
{
    if (ch==(CharType)'.') return true;
    if (ch==(CharType)',') return true;
    return false;
}

//----------------------------------------------------------------------------
template<typename CharType>
inline
int charIsSpaceOrTab( CharType ch )
{
    switch((char)ch)
    {
        case ' ' : return true;
        case '\t': return true;
        default  : return false;
    };
}

//----------------------------------------------------------------------------
inline
Decimal     fromString( const std::string &numberStr)
{
    std::string::size_type pos = 0;
    std::string::size_type sz  = numberStr.size();

    bool neg = false;

    while( charIsSpaceOrTab(numberStr[pos]) && pos!=sz ) pos++;
    if (pos==sz) throw std::runtime_error("Decimal fromString - empty string taken as number string");

    if (numberStr[pos]=='+')                        { pos++; }
    else if (numberStr[pos]=='-')                   { pos++; neg = true; }
    else if (charToDecimalDigit(numberStr[pos])>=0) {}
    else throw std::runtime_error("Decimal fromString - invalid character found");

    while( charIsSpaceOrTab(numberStr[pos]) ) pos++;

    std::int64_t  num = 0;
    std::uint32_t precision = 0;

    int dig = charToDecimalDigit(numberStr[pos]);
    while(dig>=0)
    {
        num *= 10;
        num += (std::int64_t)dig;
        pos++;
        dig = charToDecimalDigit(numberStr[pos]);
    }

    if (neg)
        num = -num;

    if (!charIsDecimalSeparator(numberStr[pos]))
    {
        return Decimal( num, DecimalPrecision(0) );
    }

    pos++;

    unsigned prec = 0;

    dig = charToDecimalDigit(numberStr[pos]);
    while( sz != pos && (dig>=0  /* && (d[pos]!=' ' && d[pos]!='\'') */ ) )
    {
        num *= 10;
        num += (std::int64_t)dig;
        pos++;
        dig = charToDecimalDigit(numberStr[pos]);
        prec++;
    }

    return Decimal::fromRawNumPrec( num, prec );

}

//----------------------------------------------------------------------------
inline
std::ostream& operator<<( std::ostream &s, const Decimal &d )
{
    s<<toString(d);
    return s;
}

//----------------------------------------------------------------------------




//----------------------------------------------------------------------------

} // namespace marty


#include <iostream>
#include "marty_decimal.h"

int main()
{
    using std::cout;
    using std::endl;

    using std::cout;
    using std::endl;

    using marty::Decimal;
    using marty::DecimalPrecision;
    using marty::toString;
    using marty::fromString;

    #define PRINT( var ) cout << #var << ": "<< var << endl;

    Decimal d1_u64_3   = (std::uint64_t)(3)  ; PRINT(d1_u64_3);
    Decimal d1_i64_7   = (std::int64_t )(7)  ; PRINT(d1_i64_7);
    Decimal d1_i64_m10 = (std::int64_t )(-10); PRINT(d1_i64_m10);
    Decimal d1_i64_15_000 = Decimal(15, DecimalPrecision(3)); PRINT(d1_i64_15_000);
    Decimal d1 = fromString("123.010");        PRINT(d1);
    Decimal d2 = fromString("13.10");          PRINT(d2);
    Decimal d1_2_sum = d1 + d2;                PRINT(d1_2_sum);  // 123.010 + 13.10 =  136.11
    Decimal d1_2_raz = d1 - d2;                PRINT(d1_2_raz);  // 123.010 - 13.10 =  109.91
    Decimal d1_2_mul = d1 * d2;                PRINT(d1_2_mul);  // 123.010 * 13.10 = 1611.431
    Decimal d1_2_div = d1 / d2;                PRINT(d1_2_div);  // 123.010 / 13.10 =    9.390076335877862595419847328
    Decimal d1_2_ost = d1 % d2;                PRINT(d1_2_ost);  // 123.010 % 13.10 =    5.11

    auto    d1_2_sum_plus_15_a = d1_2_sum + (std::int64_t )(15); PRINT(d1_2_sum_plus_15_a);
    auto    d1_2_sum_plus_15_b = d1_2_sum + (std::uint64_t)(15); PRINT(d1_2_sum_plus_15_b);
    auto    d1_2_sum_plus_15_c = d1_2_sum + (int          )(15); PRINT(d1_2_sum_plus_15_c);
    auto    d1_2_sum_plus_15_d = d1_2_sum + (unsigned     )(15); PRINT(d1_2_sum_plus_15_d);
    auto    d1_2_sum_plus_15_e = d1_2_sum + (double       )(15); PRINT(d1_2_sum_plus_15_e);
    
    return 0;

}
Маньяк Робокряк колесит по городу
Re: Decimal for C++
От: cppguard  
Дата: 27.12.20 18:47
Оценка:
Здравствуйте, Marty, Вы писали:

Зачем хранить знаменатель и другие выводимые поля в памяти, если знаменатель можно сделать всегда равным степенью двойки и хранить в виде параметра шаблона? Какой вообще смысл имеют знаменатели отличные от степеней двойки?
Re[2]: Decimal for C++
От: Marty Пират https://www.youtube.com/channel/UChp5PpQ6T4-93HbNF-8vSYg
Дата: 27.12.20 19:48
Оценка:
Здравствуйте, cppguard, Вы писали:

C>Зачем хранить знаменатель и другие выводимые поля в памяти, если знаменатель можно сделать всегда равным степенью двойки и хранить в виде параметра шаблона? Какой вообще смысл имеют знаменатели отличные от степеней двойки?


Тип называется Decimal, поэтому и знаменатель десятичный. Максимально десятичная семантика.

denum в принципе можно было бы и не хранить, а вычислять по месту из precision, это из разряда преждевременной оптимизации, просто по опыту решил, что это будет лучше. Ну и все равно, раз num — 64 бита, а для precision много не надо, то denum можно и кешировать в 32ух битной переменной.

Тут, имхо, можно было обойтись без DecimalDenumerator, и обойтись только DecimalPrecision и Decimal, ну тут хз, я предпочитаю прослойки делать почаще.

В качестве параметра шаблона точность хранить не вариант — точность у меня определяется в рантайме, и у меня можно в арифметических выражениях оперировать величинами разной точности, в результате возвращается результат с точностью, максимальной из той, что у есть операндов.

Я float/double конверсию специально сделал через строку — переложил ответственность за точность представления на стандартную либу std — "пусть лошадь думает, у неё голова большая". Потом можно будет и продумать получше этот нюанс, но пока float/doudle просто из кода пропали совсем.

Выходы за диапазоны (при всяких арифметических операциях в том числе) тоже не проверял — для финансовых расчётов всё равно никому верить нельзя, всё самому надо проверять, и входные данные, и выходное число. Тут наверное надо какие-то limits/traits допилить, чтобы было с чем сравнивать
Маньяк Робокряк колесит по городу
Re: Decimal for C++
От: ksandro Мухосранск  
Дата: 02.01.21 20:38
Оценка:
Здравствуйте, Marty, Вы писали:

M>Здравствуйте!


M>Неохота было что-то искать, накидал на скорую руку

M>...

Главное, что я не могу понять, это какой смысл было было создавать отдельные классы DecimalPrecision и DecimalDenumerator? ИМХО это переусложнение, это же просто целые числа, какой смысл делать для них классы обертки.

Зачем хранить и precision и denominator? denominator можно получить из precision очень простой операцией. Памяти твое число занимает в 1.5 раза больше, а по производительности выигрыш весьма сомнителен.

Как по мне лучше бы было без этих лишних оберток:

class Decimal {
  //...
  int num;
  int precision;
  int denominator() const;
};

Ну, вместо int может нужны другие целые типы, но принцип понятен.

При делении, мы увеличиваем точность. Что ИМХО довольно быстро приведет к runtime_error если делить что-то в цикле.
А вот при умножении, точность не меняется, что будет если 0.5 * 0.5 ?
ИМХО правильнее перегружать операторы умножения и деления на целое число. А для умножения и деления двух Decimal нужно писать отдельные функции с разной стратегией изменения точности.

И еще Denumerator было бы неплохо переименовать в Denominator,

Ну и еще на первый взгляд вижу много всяких мелочей.
Re[2]: Decimal for C++
От: Marty Пират https://www.youtube.com/channel/UChp5PpQ6T4-93HbNF-8vSYg
Дата: 03.01.21 13:00
Оценка:
Здравствуйте, ksandro, Вы писали:

K>Главное, что я не могу понять, это какой смысл было было создавать отдельные классы DecimalPrecision и DecimalDenumerator? ИМХО это переусложнение, это же просто целые числа, какой смысл делать для них классы обертки.


DecimalPrecision — чтобы задавать точность в конструкторе и не перепутать с конструктором из целого.
DecimalDenumerator — да, возможно лишний


K>Зачем хранить и precision и denominator? denominator можно получить из precision очень простой операцией. Памяти твое число занимает в 1.5 раза больше, а по производительности выигрыш весьма сомнителен.


Ну, основное число у меня — int64_t. Я решил, что выравнивание на 8 лучше чем на 4 байта для int64_t. Впрочем, довольно необоснованное предположение, но есть вероятность, что так лучше, особенно на 64х-разрядных платформах. Так что можно считать это padding'ом до нужного размера.


K>При делении, мы увеличиваем точность. Что ИМХО довольно быстро приведет к runtime_error если делить что-то в цикле.


При делении у нас precision вычитаются, при умножении — складываются. После этих операций я привожу precision результата к большей из двух операндов. При сложении/вычитании — предварительно привожу к большей из двух, и так и оставляю.


K>А вот при умножении, точность не меняется, что будет если 0.5 * 0.5 ?


0.5 * 0.5 = 0.25, точность сложилась из двух. Я тут привожу к большей из двух, но вообще тут надо подумать, может стоит оставить суммарную. Вообще, этот decimal для денег сделал, там деньги на деньги редко умножаются. Обычно на целое количество. Хотя, если брать килограммы, то они не целые, да. Но у меня килограммов нет.


K>ИМХО правильнее перегружать операторы умножения и деления на целое число. А для умножения и деления двух Decimal нужно писать отдельные функции с разной стратегией изменения точности.


Так так вроде и сделано, не?


K>И еще Denumerator было бы неплохо переименовать в Denominator,


K>Ну и еще на первый взгляд вижу много всяких мелочей.


Ну, можно и поподробнее

Спс за критику
Маньяк Робокряк колесит по городу
Re[3]: Decimal for C++
От: ksandro Мухосранск  
Дата: 03.01.21 22:28
Оценка:
Здравствуйте, Marty, Вы писали:

Code review не мой конек, к тому же вкусы у всех разные. Мне просто приходилось писать для денег несколько собственных похожих велосипедов, поэтому я смотрю на твою реализацию со своей колокольни, и может не понимаю некоторых деталей.


M>DecimalPrecision — чтобы задавать точность в конструкторе и не перепутать с конструктором из целого.

не понимаю, в чем проблема, у тебя второй precision второй параметр, он по умолчанию 0...
Decimal(2);  // num = 2, precision = 0
Decimal(2, 3)  // mum = 2, precission = 3

Что тут можно перепутать?

M>Ну, основное число у меня — int64_t. Я решил, что выравнивание на 8 лучше чем на 4 байта для int64_t. Впрочем, довольно необоснованное предположение, но есть вероятность, что так лучше, особенно на 64х-разрядных платформах. Так что можно считать это padding'ом до нужного размера.

Ну... ок, пусть будет так, хотя все равно мне это решение кажется несколько сомнительным, я бы так не делал.

K>>При делении, мы увеличиваем точность. Что ИМХО довольно быстро приведет к runtime_error если делить что-то в цикле.

M>При делении у нас precision вычитаются, при умножении — складываются. После этих операций я привожу precision результата к большей из двух операндов.
Я не разбирался детально в коде. Но у тебя в комментах написано:
// 1200.00000 / 22.000 = 54.5454545454545
// 1200 00000 / 22 000 = 5454.54545454
// 123.010 / 13.10 = 9.390076335877862595419847328
Поэтому мне показалось, что ты делаешь подругому.
Не совсем понятно, почему они должны вычитаться... Но стратегию понял, ты всегда оставляешь бОльшую точность из двух чисел (делимого и делителя или множителя)

M>При сложении/вычитании — предварительно привожу к большей из двух, и так и оставляю.

по сложению вычитанию вопросов нет


K>>А вот при умножении, точность не меняется, что будет если 0.5 * 0.5 ?


M>0.5 * 0.5 = 0.25, точность сложилась из двух. Я тут привожу к большей из двух, но вообще тут надо подумать, может стоит оставить суммарную. Вообще, этот decimal для денег сделал, там деньги на деньги редко умножаются. Обычно на целое количество. Хотя, если брать килограммы, то они не целые, да. Но у меня килограммов нет.


Вот смотри, у тебя 0.5 * 0.5 будет 0.2 совершенно не факт что это именно то, что мы хотим.




K>>ИМХО правильнее перегружать операторы умножения и деления на целое число. А для умножения и деления двух Decimal нужно писать отдельные функции с разной стратегией изменения точности.


M>Так так вроде и сделано, не?

Не совсем, ты пишешь, что деньги умножать на деньги тебе не нужно. Поэтому я предлагаю операции "Decimal * Decimal" и "Decimal / Decimal" запретить, так как неочевидно, какая должна быть точность у результата.
Вместо них можно сделать оперишции "Decimal * int" и "Decimal / int". Этих операция достаточно для рассчетов типа "скидка 5%", "налог 13%" и тд. В результате сохраняется точность исходного Decimal (точность была в копейках, то и со скидкой будет в копейках). Но, если нам все-таки надо поделить или умножить Decimal между собой, то для этого можно сделать специальные функции типа:
Decimal mul(const Decimal& x, const Decimal& y, int precision);
Decimal div(const Decimal& x, const Decimal& y, int precision);

Это толжны быть именно функции или методы с явным указанием точности с которой мы хотим получить результат. Если мы хотим получить стоимость 2.5 кило товара, то мы скорее всего заранее знаем, что мы хотим получить ее с точностью до копейки (ну или до рубля), и нам неважно с какой точностью измеряется вес (до граммов или миллиграммов). Неявное приведение точности ведет к скрытым, труднообнаружимым ошибкам.


K>>Ну и еще на первый взгляд вижу много всяких мелочей.


M>Ну, можно и поподробнее


Разглядывать код внимательно мне неохота, так что напишу первое что заметил.

Я так понимаю, что Decimal(15, DecimalPrecision(3)) даст мне в результате 15.000. Как мне получить из целых чисел дробное? Это крайне актуальная задача при парсинге бинарных биржевых протоколов (типа ITCH). Там мы получаем цену в виде целого числа (например 12345) с известной точностью точностью (нпример 4 знака после запятой). Это значит что цена 1.2345.

Конструктор из числа с плавающей точкой лучше убрать. Ты конечно сделал конвертацию через строку, что позволяет избежать ошибок типа когда Decimal(0.3) превращается в 0.299999. Но все равно кто-нибудь обязательно сделает Decimal из результата вычислений с плавающей точкой типа Decimal(1.0/3.0) какая тут будет точность? А если так Decimal(1.0 / 30000000.0), тут вообще все будет очень весело!!!

Функуии toString и fromString можно бы сделать статическими методами, да и вообще можно бы сделать конструктор из строки (очень актуально для парсинга текстовых биржевых протоколов, типа FIX, ну или json).

В большинстве случаев точность можно задавать compile time через параметр шаблона, работать будет быстрее и памяти меньше занимать. Функцуиональности часто достаточно. Задал что точность 2 знака после запятой (копейки) и с такой точностью и считаешь. Но я не знаю, твою задачу, кое где может понадобиться и переменная точность.
Re[4]: Decimal for C++
От: Marty Пират https://www.youtube.com/channel/UChp5PpQ6T4-93HbNF-8vSYg
Дата: 03.01.21 23:42
Оценка:
Здравствуйте, ksandro, Вы писали:

K>Code review не мой конек, к тому же вкусы у всех разные. Мне просто приходилось писать для денег несколько собственных похожих велосипедов, поэтому я смотрю на твою реализацию со своей колокольни, и может не понимаю некоторых деталей.


Все равно спс


M>>DecimalPrecision — чтобы задавать точность в конструкторе и не перепутать с конструктором из целого.

K>не понимаю, в чем проблема, у тебя второй precision второй параметр, он по умолчанию 0...
K>
K>Decimal(2);  // num = 2, precision = 0
K>Decimal(2, 3)  // mum = 2, precission = 3
K>

K>Что тут можно перепутать?

Очередность параметров. Где precision, а где value. Налюбится тут гораздо легче, чем кажется


M>>Ну, основное число у меня — int64_t. Я решил, что выравнивание на 8 лучше чем на 4 байта для int64_t. Впрочем, довольно необоснованное предположение, но есть вероятность, что так лучше, особенно на 64х-разрядных платформах. Так что можно считать это padding'ом до нужного размера.

K>Ну... ок, пусть будет так, хотя все равно мне это решение кажется несколько сомнительным, я бы так не делал.

Ну и была бы кривая структура 96 бит размером. Тоже такое себе. Ну и какая-никакая, но просадка в производительности, когда каждый раз denum из precision делаешь. Но кривое выравнивание — это имхо бОльшая проблема


K>>>При делении, мы увеличиваем точность. Что ИМХО довольно быстро приведет к runtime_error если делить что-то в цикле.

M>>При делении у нас precision вычитаются, при умножении — складываются. После этих операций я привожу precision результата к большей из двух операндов.
K>Я не разбирался детально в коде. Но у тебя в комментах написано:
K> // 1200.00000 / 22.000 = 54.5454545454545
K> // 1200 00000 / 22 000 = 5454.54545454
K> // 123.010 / 13.10 = 9.390076335877862595419847328
K>Поэтому мне показалось, что ты делаешь подругому.
K>Не совсем понятно, почему они должны вычитаться... Но стратегию понял, ты всегда оставляешь бОльшую точность из двух чисел (делимого и делителя или множителя)

Да мне тоже думать было лень, было смутное подозрение, я его проверил, оно оказалось верным (на самом деле просто старые знания проверил).
Еще раз глянул код — перед делением я ещё делал minimizePrecision для делителя — когда лишние нули в конце после точки убираются.

Я в свое время делал трехбайтный float для спектрума, оттуда остаточные обрывки знаний и остались. Про IEE я тогда не знал, а возможно их и не было ещё, сам до всего доходил, двух байт показалось недостаточно для нужной точности, а на выравнивание там было пох.


M>>При сложении/вычитании — предварительно привожу к большей из двух, и так и оставляю.

K>по сложению вычитанию вопросов нет


K>>>А вот при умножении, точность не меняется, что будет если 0.5 * 0.5 ?


M>>0.5 * 0.5 = 0.25, точность сложилась из двух. Я тут привожу к большей из двух, но вообще тут надо подумать, может стоит оставить суммарную. Вообще, этот decimal для денег сделал, там деньги на деньги редко умножаются. Обычно на целое количество. Хотя, если брать килограммы, то они не целые, да. Но у меня килограммов нет.


K>Вот смотри, у тебя 0.5 * 0.5 будет 0.2 совершенно не факт что это именно то, что мы хотим.


Тут я наврал сгоряча. При умножении изначально ничего не трогал, но сейчас ещё добавил minimizePrecision для делителя и для результата. Надо будет обновить на страничке сорцов.


K>>>ИМХО правильнее перегружать операторы умножения и деления на целое число. А для умножения и деления двух Decimal нужно писать отдельные функции с разной стратегией изменения точности.


M>>Так так вроде и сделано, не?

K>Не совсем, ты пишешь, что деньги умножать на деньги тебе не нужно. Поэтому я предлагаю операции "Decimal * Decimal" и "Decimal / Decimal" запретить, так как неочевидно, какая должна быть точность у результата.
K>Вместо них можно сделать оперишции "Decimal * int" и "Decimal / int". Этих операция достаточно для рассчетов типа "скидка 5%", "налог 13%" и тд. В результате сохраняется точность исходного Decimal (точность была в копейках, то и со скидкой будет в копейках). Но, если нам все-таки надо поделить или умножить Decimal между собой, то для этого можно сделать специальные функции типа:
K>
K>Decimal mul(const Decimal& x, const Decimal& y, int precision);
K>Decimal div(const Decimal& x, const Decimal& y, int precision);
K>

K>Это толжны быть именно функции или методы с явным указанием точности с которой мы хотим получить результат. Если мы хотим получить стоимость 2.5 кило товара, то мы скорее всего заранее знаем, что мы хотим получить ее с точностью до копейки (ну или до рубля), и нам неважно с какой точностью измеряется вес (до граммов или миллиграммов). Неявное приведение точности ведет к скрытым, труднообнаружимым ошибкам.

Я тебя понял. "Decimal * Decimal" и "Decimal / Decimal" — точность авто, сейчас ещё добавил минимизацию точности при умножении. Идея в том, чтобы быть как встроенный тип без каких-либо нюансов, но хранить ровно то, то требуется. Тут нужно только указание точности при форматировании правильно указывать.


K>>>Ну и еще на первый взгляд вижу много всяких мелочей.


M>>Ну, можно и поподробнее


K>Разглядывать код внимательно мне неохота, так что напишу первое что заметил.


K>Я так понимаю, что Decimal(15, DecimalPrecision(3)) даст мне в результате 15.000. Как мне получить из целых чисел дробное? Это крайне актуальная задача при парсинге бинарных биржевых протоколов (типа ITCH). Там мы получаем цену в виде целого числа (например 12345) с известной точностью точностью (нпример 4 знака после запятой). Это значит что цена 1.2345.


Вот не понял, в чём проблема. Я как раз это замутил, потому что пишу плюсовый REST клиент под тинковское брокерское REST API


K>Конструктор из числа с плавающей точкой лучше убрать. Ты конечно сделал конвертацию через строку, что позволяет избежать ошибок типа когда Decimal(0.3) превращается в 0.299999. Но все равно кто-нибудь обязательно сделает Decimal из результата вычислений с плавающей точкой типа Decimal(1.0/3.0) какая тут будет точность? А если так Decimal(1.0 / 30000000.0), тут вообще все будет очень весело!!!


Тут я через жопу стандартной библиотеки сделал, да, и всю ответственность переложил на неё. Но Decimal from float/double не считаю нужным запрещать. Тут каждый сам себе буратино. Не считаю нужным отказывать желающим походить по граблям походить по ним.


K>Функуии toString и fromString можно бы сделать статическими методами, да и вообще можно бы сделать конструктор из строки (очень актуально для парсинга текстовых биржевых протоколов, типа FIX, ну или json).


Возможно, может пересмотрю как-нибудь. Пока я весь тинковский REST из какого-то сваггера смог в Qt/C++ оттранслировать, а все даблы из сгенерённого ручками заменил на свой Decimal. Это показалось мне более простым, чем править какой-то мутный генератор из сваггера, написанный на джаве, там всякие мавины сразу лезут, и вообще там полная пипецома


K>В большинстве случаев точность можно задавать compile time через параметр шаблона, работать будет быстрее и памяти меньше занимать. Функцуиональности часто достаточно. Задал что точность 2 знака после запятой (копейки) и с такой точностью и считаешь. Но я не знаю, твою задачу, кое где может понадобиться и переменная точность.


Я дергаю это через REST и сохраняю в SQL, так что ваще пофик на скорость. Переменная точность нужна просто для того, чтобы на разумных тащем-то значениях входных данных вылететь за пределы
Маньяк Робокряк колесит по городу
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.