[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 22:44:43 +0000 (UTC) |
branch: master
commit 6d3def692e2b0c430ba45797655d77c9eec74b68
Author: Gregory W. Chicares <address@hidden>
Date: Sat Jun 18 18:19:18 2016 +0000
Replace abstract units and subunits with dollars and cents
---
currency.hpp | 196 ++++++++++++++++++++++++++++++-----------------------
currency_test.cpp | 79 ++++++++++-----------
2 files changed, 153 insertions(+), 122 deletions(-)
diff --git a/currency.hpp b/currency.hpp
index b547e7a..eb61756 100644
--- a/currency.hpp
+++ b/currency.hpp
@@ -1,4 +1,4 @@
-// Represent an amount in currency units and subunits.
+// Represent a currency amount exactly as integral cents.
//
// Copyright (C) 2016 Gregory W. Chicares.
//
@@ -32,73 +32,81 @@
#include <limits>
#include <stdexcept>
-/// Represent a monetary amount as an exact number of base units and subunits.
+/// Represent a currency amount exactly as integral cents.
///
-/// 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.
+/// This class is tailored to US currency, as lmi is tailored to US
+/// life insurance.
///
-/// 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.
+/// By storing the amount as an integer number of cents internally,
+/// this class avoids roundoff error for addition and subtraction.
+/// For multiplicative operations, conversions to and from double
+/// point type are provided; it is the caller's responsibility to
+/// round the final result of such calculations 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.
+/// This class provides value-like semantics and has a small size,
+/// making it appropriate to pass instances by value instead of the
+/// 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 int32_t for the value would limit the range to about
+ /// twenty million dollars, which is insufficient; but int32_t
+ /// accommodates values up to about ninety quadrillion dollars,
+ /// which is enough for any life insurance contract in 2016.
+
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 int cents_digits = 2;
+ static constexpr int cents_per_dollar = 100; // std::pow(10, cents_digits)
- static constexpr amount_type max_units()
+ static constexpr amount_type max_dollars()
{
- return std::numeric_limits<amount_type>::max() / subunits_per_unit;
+ return std::numeric_limits<amount_type>::max() / cents_per_dollar;
}
- // Default-initialized currency objects is 0.
+ /// No ctor-initializer-list is needed because the lone data
+ /// member has a member-initializer at its point of declaration.
+
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)
+ /// Constructor from a positive number of dollars and cents.
+ ///
+ /// The cents argument must be normalized: i.e., positive and
+ /// strictly less than cents_per_dollar.
+
+ currency(amount_type dollars, int cents)
{
- if(units < 0 || units >= max_units())
+ if(dollars < 0 || dollars >= max_dollars())
{
throw std::overflow_error("Currency amount out of range.");
}
- if(subunits < 0 || subunits >= subunits_per_unit)
+ if(cents < 0 || cents >= cents_per_dollar)
{
- throw std::runtime_error("Invalid amount of currency subunits.");
+ throw std::runtime_error("Invalid number of cents.");
}
- subunits_ = subunits_per_unit*units + subunits;
+ cents_ = cents_per_dollar * dollars + cents;
}
- // Static constructor from the fractional amount of units rounding them to
- // the nearest subunit. The argument may be positive or negative.
+ /// Static constructor from floating-point dollars.
+ ///
+ /// The argument may be positive or negative. Its value is rounded
+ /// to the nearest cent.
+
static currency from_value(double d)
{
- if(std::trunc(d) >= static_cast<double>(max_units()))
+ if(std::trunc(d) >= static_cast<double>(max_dollars()))
{
throw std::overflow_error("Currency amount out of range.");
}
- // The check above ensures that the product fits into amount_type.
+ // The check above ensures that the value fits in amount_type.
return currency
- (static_cast<amount_type>(std::round(subunits_per_unit*d))
+ (static_cast<amount_type>(std::round(cents_per_dollar * d))
);
}
@@ -108,74 +116,83 @@ class currency
// Accessors.
- // The number of units may be negative.
- amount_type units() const
+ /// Number of whole dollars. May be negative.
+
+ amount_type dollars() const
{
- return subunits_ / subunits_per_unit;
+ return cents_ / cents_per_dollar;
}
- // 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
+
+ /// Number of whole cents. May be negative.
+ ///
+ /// The number of cents must be negative if the number of dollars
+ /// is negative. The number of cents may be negative if the number
+ /// of dollars is zero. Otherwise the number of cents must be
+ /// nonnegative.
+
+ int cents() const
{
- return subunits_ % subunits_per_unit;
+ return cents_ % cents_per_dollar;
}
- // Total number of subunits, i.e. 123 for 1 unit and 23 subunits.
- amount_type total_subunits() const
+ /// Total number of cents, e.g., 123 for 1 dollar and 23 cents.
+
+ amount_type total_cents() const
{
- return subunits_;
+ return cents_;
}
- // Value in terms of units, to be used for arithmetic operations not
- // provided by this class itself.
+ /// Value as floating-point dollars, for mixed-mode arithmetic.
+
double value() const
{
- double result = subunits_;
- result /= subunits_per_unit;
+ double result = cents_;
+ result /= cents_per_dollar;
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_; }
+ bool operator< (currency other) const { return cents_ < other.cents_; }
+ bool operator<=(currency other) const { return cents_ <= other.cents_; }
+ bool operator==(currency other) const { return cents_ == other.cents_; }
+ bool operator!=(currency other) const { return cents_ != other.cents_; }
+ bool operator> (currency other) const { return cents_ > other.cents_; }
+ bool operator>=(currency other) const { return cents_ >= other.cents_; }
// Arithmetic operations.
currency operator-() const
{
- return currency(-subunits_);
+ return currency(-cents_);
}
currency& operator+=(currency other)
{
- subunits_ += other.subunits_;
+ cents_ += other.cents_;
return *this;
}
currency& operator-=(currency other)
{
- subunits_ -= other.subunits_;
+ cents_ -= other.cents_;
return *this;
}
currency& operator*=(int factor)
{
- subunits_ *= factor;
+ cents_ *= 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)
+ /// This ctor is only used internally: it is too error-prone to
+ /// expose it publicly.
+
+ explicit currency(amount_type cents)
+ :cents_(cents)
{
}
- amount_type subunits_ = 0;
+ amount_type cents_ = 0;
};
inline currency operator+(currency lhs, currency rhs)
@@ -198,43 +215,54 @@ inline currency operator*(int lhs, currency rhs)
return currency(rhs) *= lhs;
}
+/// Insert the dollars-and-cents amount into a stream.
+///
+/// Dollars and cents, being exact integers, are formatted separately,
+/// but the negative sign cannot be supplied by either of those two
+/// separate formatting operations: $-12.34 must not be inserted as
+/// "-12.-34"; and $-0.56 must be inserted as "-0.56" even though the
+/// whole-dollar amount is not negative.
+///
+/// The decimal mark is hard-coded as '.' because that is universal
+/// US practice.
+
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)
+ if(c.total_cents() < 0)
{
os << '-';
}
return os
- << std::abs(c.units())
+ << std::abs(c.dollars())
<< '.'
<< std::setfill('0')
- << std::setw(currency::subunits_digits)
- << std::abs(c.subunits());
+ << std::setw(currency::cents_digits)
+ << std::abs(c.cents());
}
+/// Extract a dollars-and-cents amount from a stream.
+///
+/// The negative sign requires special attention so that $-0.56 is not
+/// extracted as -0 dollars plus 56 cents.
+///
+/// The decimal mark is hard-coded as '.' because that is universal
+/// US practice.
+
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;
+ currency::amount_type dollars = 0;
+ is >> dollars;
if(!is)
+ {
return is;
+ }
if(is.get() != '.')
{
@@ -242,18 +270,20 @@ inline std::istream& operator>>(std::istream& is,
currency& c)
return is;
}
- int subunits = 0;
- is >> subunits;
+ int cents = 0;
+ is >> cents;
if(!is)
+ {
return is;
+ }
- if(subunits < 0 || subunits >= currency::subunits_per_unit)
+ if(cents < 0 || cents >= currency::cents_per_dollar)
{
is.setstate(std::ios_base::failbit);
return is;
}
- c = currency(units, subunits);
+ c = currency(dollars, cents);
if(negative)
{
c = -c;
diff --git a/currency_test.cpp b/currency_test.cpp
index 44e099c..2603e8d 100644
--- a/currency_test.cpp
+++ b/currency_test.cpp
@@ -1,4 +1,4 @@
-// Currency amounts--unit test.
+// Represent a currency amount exactly as integral cents--unit test.
//
// Copyright (C) 2016 Gregory W. Chicares.
//
@@ -31,87 +31,88 @@
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);
+ BOOST_TEST_EQUAL(currency( ).total_cents(), 0);
+ BOOST_TEST_EQUAL(currency(0, 99).total_cents(), 99);
+ BOOST_TEST_EQUAL(currency(1, 99).total_cents(), 199);
currency const c(4, 56);
- BOOST_TEST_EQUAL(currency(c).total_subunits(), 456);
+ BOOST_TEST_EQUAL(currency(c).total_cents(), 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);
+ static char const* const overflow_msg = "Currency amount out of range.";
+ BOOST_TEST_THROW(currency(-1, 0), std::overflow_error, overflow_msg);
+ BOOST_TEST_THROW(currency(-1, 99), std::overflow_error, overflow_msg);
+ BOOST_TEST_THROW(currency(-1, -99), std::overflow_error, overflow_msg);
BOOST_TEST_THROW
(currency(std::numeric_limits<currency::amount_type>::max(), 0)
,std::overflow_error
- ,range_error
+ ,overflow_msg
);
BOOST_TEST_THROW
(currency(std::numeric_limits<currency::amount_type>::min(), 0)
,std::overflow_error
- ,range_error
+ ,overflow_msg
);
- 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);
+ static char const* const cents_msg = "Invalid number of cents.";
+ BOOST_TEST_THROW(currency(1, 100), std::runtime_error, cents_msg);
+ BOOST_TEST_THROW(currency(1, 101), std::runtime_error, cents_msg);
+ BOOST_TEST_THROW(currency(1, -1), std::runtime_error, cents_msg);
}
void test_accessors()
{
auto c = currency(1234, 56);
- BOOST_TEST_EQUAL(c.units(), 1234);
- BOOST_TEST_EQUAL(c.subunits(), 56);
+ BOOST_TEST_EQUAL(c.dollars(), 1234);
+ BOOST_TEST_EQUAL(c.cents() , 56);
c = -currency(9876543, 21);
- BOOST_TEST_EQUAL(c.units(), -9876543);
- BOOST_TEST_EQUAL(c.subunits(), -21);
+ BOOST_TEST_EQUAL(c.dollars(), -9876543);
+ BOOST_TEST_EQUAL(c.cents() , -21);
c = -currency(0, 99);
- BOOST_TEST_EQUAL(c.units(), 0);
- BOOST_TEST_EQUAL(c.subunits(), -99);
+ BOOST_TEST_EQUAL(c.dollars(), 0);
+ BOOST_TEST_EQUAL(c.cents() , -99);
c = -c;
- BOOST_TEST_EQUAL(c.units(), 0);
- BOOST_TEST_EQUAL(c.subunits(), 99);
+ BOOST_TEST_EQUAL(c.dollars(), 0);
+ BOOST_TEST_EQUAL(c.cents() , 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, 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) );
+ 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);
+ BOOST_TEST_EQUAL(c.total_cents(), 600);
c *= 12;
- BOOST_TEST_EQUAL(c.total_subunits(), 7200);
+ BOOST_TEST_EQUAL(c.total_cents(), 7200);
+ // $72.00 - $80.10 = $8.10
auto d = c - currency(80, 10);
- BOOST_TEST_EQUAL(d.total_subunits(), -810);
+ BOOST_TEST_EQUAL(d.total_cents(), -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( 1.23).total_cents(), 123);
+ BOOST_TEST_EQUAL(currency::from_value(-1.23).total_cents(), -123);
- BOOST_TEST_EQUAL(currency::from_value(0.005).total_subunits(), 1);
- BOOST_TEST_EQUAL(currency::from_value(-0.005).total_subunits(), -1);
+ BOOST_TEST_EQUAL(currency::from_value( 0.005).total_cents(), 1);
+ BOOST_TEST_EQUAL(currency::from_value(-0.005).total_cents(), -1);
- auto c = currency::from_value(14857345.859999999404);
- BOOST_TEST_EQUAL(c.total_subunits() ,1485734586);
- BOOST_TEST_EQUAL(c.value(), 14857345.86);
+ auto c = currency::from_value( 14857345.859999999404);
+ BOOST_TEST_EQUAL(c.total_cents() ,1485734586);
+ BOOST_TEST_EQUAL(c.value() ,14857345.86);
}
void test_stream_roundtrip