lmi-commits
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[lmi-commits] (no subject)


From: Greg Chicares
Subject: [lmi-commits] (no subject)
Date: Sat, 18 Jun 2016 11:54:00 +0000 (UTC)

branch: master
commit 84828e428b118927486096abf3c4fe78e1a508aa
Author: Gregory W. Chicares <address@hidden>
Date:   Fri Jun 17 23:03:44 2016 +0000

    Introduce currency class representing values as integral cents (VZ)
    
    This is the original prototype announced here:
      http://lists.nongnu.org/archive/html/lmi/2016-05/msg00007.html
    
    This class traffics in the smallest currency unit. Assuming that unit
    to be atomic, additive currency operations are exact: i.e., immune
    to roundoff error.
    
    Conversions to and from text are provided. Experimental conversions
    to and from 'double' support mixed-mode arithmetic.
---
 Makefile.am       |    6 ++
 currency.hpp      |  267 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 currency_test.cpp |  167 +++++++++++++++++++++++++++++++++
 objects.make      |    5 +
 4 files changed, 445 insertions(+)

diff --git a/Makefile.am b/Makefile.am
index b59998c..517c96d 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -96,6 +96,7 @@ TESTS = \
     test_configurable_settings \
     test_contains \
     test_crc32 \
+    test_currency \
     test_expression_template_0 \
     test_fenv_lmi \
     test_file_command \
@@ -633,6 +634,11 @@ test_crc32_SOURCES = \
   crc32_test.cpp
 test_crc32_CXXFLAGS = $(AM_CXXFLAGS)
 
+test_currency_SOURCES = \
+  $(common_test_objects) \
+  currency_test.cpp
+test_currency_CXXFLAGS = $(AM_CXXFLAGS)
+
 test_expression_template_0_SOURCES = \
   $(common_test_objects) \
   expression_template_0_test.cpp \
diff --git a/currency.hpp b/currency.hpp
new file mode 100644
index 0000000..8537962
--- /dev/null
+++ b/currency.hpp
@@ -0,0 +1,267 @@
+// Represent an amount in currency units and subunits.
+//
+// Copyright (C) 2016 Gregory W. Chicares.
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software Foundation,
+// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+//
+// http://savannah.nongnu.org/projects/lmi
+// email: <address@hidden>
+// snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+
+// $Id$
+
+#ifndef currency_hpp
+#define currency_hpp
+
+#include "config.hpp"
+
+#include <cmath>
+#include <cstdint>
+#include <cstdlib>
+#include <iomanip>
+#include <iostream>
+#include <limits>
+#include <stdexcept>
+
+/// Represent a monetary amount as an exact number of base units and subunits.
+///
+/// This class currently assumes that there are 100 subunits in the base unit,
+/// which is not the case for all currencies, but definitely is for USD, which
+/// is the only currency used in lmi.
+///
+/// By storing the amount as an integer number of units and subunits internally
+/// this class avoids rounding errors for the operations involving additions
+/// and subtractions. For the multiplicative operations, conversions to and
+/// from floating point type are provided and it is the caller responsibility
+/// to correctly round the final result of a calculation involving such
+/// operations to a currency amount.
+///
+/// This class provides value-like semantics and has a small size, making it
+/// appropriate to pass it by value instead of more usual const reference.
+
+class currency
+{
+  public:
+    // Using int32_t for the value would limit this class to ~200 million units
+    // which is insufficient, so use 64 bit type which allows to represent
+    // values up to almost 1 quintillion which should be sufficient.
+    using amount_type = std::int_fast64_t;
+
+    static constexpr int subunits_digits = 2;
+    static constexpr int subunits_per_unit = 100; // std::pow(10, 
subunits_digits)
+
+    static constexpr amount_type max_units()
+        {
+        return std::numeric_limits<amount_type>::max() / subunits_per_unit;
+        }
+
+    // Default-initialized currency objects is 0.
+    currency()
+        {
+        }
+
+    // Constructor from a positive number of units and subunits. The subunits
+    // argument must be normalized i.e. positive and strictly less than
+    // subunits_per_unit.
+    currency(amount_type units, int subunits)
+        {
+        if(units < 0 || units >= max_units())
+            {
+            throw std::overflow_error("Currency amount out of range.");
+            }
+
+        if(subunits < 0 || subunits >= subunits_per_unit)
+            {
+            throw std::runtime_error("Invalid amount of currency subunits.");
+            }
+
+        subunits_ = subunits_per_unit*units + subunits;
+        }
+
+    // Static constructor from the fractional amount of units rounding them to
+    // the nearest subunit. The argument may be positive or negative.
+    static currency from_value(double d)
+        {
+        if(std::trunc(d) >= static_cast<double>(max_units()))
+            {
+            throw std::overflow_error("Currency amount out of range.");
+            }
+
+        // The check above ensures that the product fits into amount_type.
+        return currency
+            (static_cast<amount_type>(std::round(subunits_per_unit*d))
+            );
+        }
+
+    currency(currency const&) = default;
+    currency& operator=(currency const&) = default;
+    ~currency() = default;
+
+    // Accessors.
+
+    // The number of units may be negative.
+    amount_type units() const
+        {
+        return subunits_ / subunits_per_unit;
+        }
+    // The number of subunits may also be negative and will always be if the
+    // number of units is, i.e. -12.34 is (-12) units + (-34) subunits.
+    int subunits() const
+        {
+        return subunits_ % subunits_per_unit;
+        }
+
+    // Total number of subunits, i.e. 123 for 1 unit and 23 subunits.
+    amount_type total_subunits() const
+        {
+        return subunits_;
+        }
+
+    // Value in terms of units, to be used for arithmetic operations not
+    // provided by this class itself.
+    double value() const
+        {
+        double result = subunits_;
+        result /= subunits_per_unit;
+        return result;
+        }
+
+    // Comparisons.
+    bool operator< (currency other) const { return subunits_ <  
other.subunits_; }
+    bool operator<=(currency other) const { return subunits_ <= 
other.subunits_; }
+    bool operator==(currency other) const { return subunits_ == 
other.subunits_; }
+    bool operator!=(currency other) const { return subunits_ != 
other.subunits_; }
+    bool operator> (currency other) const { return subunits_ >  
other.subunits_; }
+    bool operator>=(currency other) const { return subunits_ >= 
other.subunits_; }
+
+    // Arithmetic operations.
+    currency operator-() const
+        {
+        return currency(-subunits_);
+        }
+
+    currency& operator+=(currency other)
+        {
+        subunits_ += other.subunits_;
+        return *this;
+        }
+
+    currency& operator-=(currency other)
+        {
+        subunits_ -= other.subunits_;
+        return *this;
+        }
+
+    currency& operator*=(int factor)
+        {
+        subunits_ *= factor;
+        return *this;
+        }
+
+  private:
+    // This ctor is only used internally, it is too error-prone to expose it
+    // publicly.
+    explicit currency(amount_type subunits)
+        :subunits_(subunits)
+        {
+        }
+
+    amount_type subunits_ = 0;
+};
+
+inline currency operator+(currency lhs, currency rhs)
+{
+    return currency(lhs) += rhs;
+}
+
+inline currency operator-(currency lhs, currency rhs)
+{
+    return currency(lhs) -= rhs;
+}
+
+inline currency operator*(currency lhs, int rhs)
+{
+    return currency(lhs) *= rhs;
+}
+
+inline currency operator*(int lhs, currency rhs)
+{
+    return currency(rhs) *= lhs;
+}
+
+inline std::ostream& operator<<(std::ostream& os, currency c)
+{
+    // When formatting a negative currency amount, there should be no sign
+    // before the number of subunits: "-12.34" and not "-12.-34". On the other
+    // hand side, we need to output the sign manually for negative amount
+    // because we can't rely on it appearing as part of c.units(): this doesn't
+    // work when units amount is 0.
+    //
+    // Decimal point is currently hard-coded as it is always the appropriate
+    // separator to use for lmi.
+    if(c.total_subunits() < 0)
+        {
+        os << '-';
+        }
+
+    return os
+        << std::abs(c.units())
+        << '.'
+        << std::setfill('0')
+        << std::setw(currency::subunits_digits)
+        << std::abs(c.subunits());
+}
+
+inline std::istream& operator>>(std::istream& is, currency& c)
+{
+    // We can't rely on comparing units with 0 as this doesn't work for
+    // "-0.xx", so test for the sign ourselves and skip it if it's there.
+    bool const negative = is.peek() == '-';
+    if(negative)
+        {
+        is.get();
+        }
+
+    currency::amount_type units = 0;
+    is >> units;
+    if(!is)
+        return is;
+
+    if(is.get() != '.')
+        {
+        is.setstate(std::ios_base::failbit);
+        return is;
+        }
+
+    int subunits = 0;
+    is >> subunits;
+    if(!is)
+        return is;
+
+    if(subunits < 0 || subunits >= currency::subunits_per_unit)
+        {
+        is.setstate(std::ios_base::failbit);
+        return is;
+        }
+
+    c = currency(units, subunits);
+    if(negative)
+        {
+        c = -c;
+        }
+
+    return is;
+}
+
+#endif // currency_hpp
diff --git a/currency_test.cpp b/currency_test.cpp
new file mode 100644
index 0000000..8597211
--- /dev/null
+++ b/currency_test.cpp
@@ -0,0 +1,167 @@
+// Currency amounts--unit test.
+//
+// Copyright (C) 2016 Gregory W. Chicares.
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software Foundation,
+// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+//
+// http://savannah.nongnu.org/projects/lmi
+// email: <address@hidden>
+// snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+
+// $Id$
+
+#ifdef __BORLANDC__
+#   include "pchfile.hpp"
+#   pragma hdrstop
+#endif // __BORLANDC__
+
+#include "currency.hpp"
+
+#include "test_tools.hpp"
+
+#include <limits>
+#include <sstream>
+#include <stdexcept>
+
+void test_ctors()
+{
+    BOOST_TEST_EQUAL(currency().total_subunits(), 0);
+    BOOST_TEST_EQUAL(currency(0, 99).total_subunits(), 99);
+    BOOST_TEST_EQUAL(currency(1, 99).total_subunits(), 199);
+
+    currency const c(4, 56);
+    BOOST_TEST_EQUAL(currency(c).total_subunits(), 456);
+
+    static char const* const range_error = "Currency amount out of range.";
+    BOOST_TEST_THROW(currency(-1, 0), std::overflow_error, range_error);
+    BOOST_TEST_THROW(currency(-1, 99), std::overflow_error, range_error);
+    BOOST_TEST_THROW(currency(-1, -99), std::overflow_error, range_error);
+    BOOST_TEST_THROW
+        (currency(std::numeric_limits<currency::amount_type>::max(), 0)
+        ,std::overflow_error
+        ,range_error
+        );
+    BOOST_TEST_THROW
+        (currency(std::numeric_limits<currency::amount_type>::min(), 0)
+        ,std::overflow_error
+        ,range_error
+        );
+
+    static char const* const subunits_error = "Invalid amount of currency 
subunits.";
+    BOOST_TEST_THROW(currency(1, 100), std::runtime_error, subunits_error);
+    BOOST_TEST_THROW(currency(1, 101), std::runtime_error, subunits_error);
+    BOOST_TEST_THROW(currency(1, -1), std::runtime_error, subunits_error);
+}
+
+void test_accessors()
+{
+    auto c = currency(1234, 56);
+    BOOST_TEST_EQUAL(c.units(), 1234);
+    BOOST_TEST_EQUAL(c.subunits(), 56);
+
+    c = -currency(9876543, 21);
+    BOOST_TEST_EQUAL(c.units(), -9876543);
+    BOOST_TEST_EQUAL(c.subunits(), -21);
+
+    c = -currency(0, 99);
+    BOOST_TEST_EQUAL(c.units(), 0);
+    BOOST_TEST_EQUAL(c.subunits(), -99);
+
+    c = -c;
+    BOOST_TEST_EQUAL(c.units(), 0);
+    BOOST_TEST_EQUAL(c.subunits(), 99);
+}
+
+void test_comparison()
+{
+    BOOST_TEST( currency(1, 23) < currency(1, 24) );
+    BOOST_TEST( -currency(1, 23) > -currency(1, 24) );
+
+    BOOST_TEST( currency(1, 23) <= currency(1, 23) );
+    BOOST_TEST( currency(1, 23) == currency(1, 23) );
+    BOOST_TEST( currency(1, 23) != currency(1, 24) );
+    BOOST_TEST( currency(1, 23) >= currency(1, 23) );
+}
+
+void test_arithmetic()
+{
+    auto c = currency(1, 23) + currency(4, 77);
+    BOOST_TEST_EQUAL(c.total_subunits(), 600);
+
+    c *= 12;
+    BOOST_TEST_EQUAL(c.total_subunits(), 7200);
+
+    auto d = c - currency(80, 10);
+    BOOST_TEST_EQUAL(d.total_subunits(), -810);
+}
+
+void test_double()
+{
+    BOOST_TEST_EQUAL(currency::from_value(1.23).total_subunits(), 123);
+    BOOST_TEST_EQUAL(currency::from_value(-1.23).total_subunits(), -123);
+
+    BOOST_TEST_EQUAL(currency::from_value(0.005).total_subunits(), 1);
+    BOOST_TEST_EQUAL(currency::from_value(-0.005).total_subunits(), -1);
+
+    auto c = currency::from_value(14857345.859999999404);
+    BOOST_TEST_EQUAL(c.total_subunits() ,1485734586);
+    BOOST_TEST_EQUAL(c.value(), 14857345.86);
+}
+
+void test_stream_roundtrip
+    (currency c0
+    ,std::string const& str
+    ,char const* file
+    ,int line
+    )
+{
+    std::stringstream ss;
+    currency c;
+
+    ss << c0;
+    INVOKE_BOOST_TEST_EQUAL(ss.str(), str, file, line);
+    ss >> c;
+    INVOKE_BOOST_TEST( ss.eof (), file, line);
+    INVOKE_BOOST_TEST(!ss.fail(), file, line);
+    INVOKE_BOOST_TEST(!ss.bad (), file, line);
+    INVOKE_BOOST_TEST_EQUAL(c, c0, file, line);
+}
+
+void test_streams()
+{
+    #define TEST_ROUNDTRIP(c, str) \
+        test_stream_roundtrip(c, str, __FILE__, __LINE__)
+
+    TEST_ROUNDTRIP( currency(123, 45),  "123.45");
+    TEST_ROUNDTRIP( currency(  0,  0),    "0.00");
+    TEST_ROUNDTRIP( currency(  0,  1),    "0.01");
+    TEST_ROUNDTRIP( currency(  0, 99),    "0.99");
+    TEST_ROUNDTRIP(-currency(123, 45), "-123.45");
+    TEST_ROUNDTRIP(-currency(  0,  1),   "-0.01");
+    TEST_ROUNDTRIP(-currency(  0, 99),   "-0.99");
+
+    #undef TEST_ROUNDTRIP
+}
+
+int test_main(int, char*[])
+{
+    test_ctors();
+    test_accessors();
+    test_comparison();
+    test_arithmetic();
+    test_double();
+    test_streams();
+
+    return EXIT_SUCCESS;
+}
diff --git a/objects.make b/objects.make
index 7b26bf1..461da2b 100644
--- a/objects.make
+++ b/objects.make
@@ -405,6 +405,7 @@ unit_test_targets := \
   configurable_settings_test \
   contains_test \
   crc32_test \
+  currency_test \
   expression_template_0_test \
   fenv_lmi_test \
   file_command_test \
@@ -564,6 +565,10 @@ crc32_test$(EXEEXT): \
   crc32.o \
   crc32_test.o \
 
+currency_test$(EXEEXT): \
+  $(common_test_objects) \
+  currency_test.o \
+
 expression_template_0_test$(EXEEXT): \
   $(common_test_objects) \
   expression_template_0_test.o \



reply via email to

[Prev in Thread] Current Thread [Next in Thread]