[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 \