use std::fmt::{self, Debug, Display, Formatter};
use std::ops::{Add, Neg, Sub};
use std::str::FromStr;
#[derive(Clone, Copy, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Dollars {
cent_value: i64,
}
impl Dollars {
pub fn dollars(&self) -> i64 {
(self.cent_value / 100).abs()
}
pub fn cents(&self) -> i64 {
(self.cent_value % 100).abs()
}
pub fn in_cents(&self) -> i64 {
self.cent_value
}
pub fn is_positive(&self) -> bool {
self.in_cents() > 0
}
}
impl Add for Dollars {
type Output = Self;
fn add(self, other: Self) -> Self::Output {
Self::from(self.in_cents() + other.in_cents())
}
}
impl Debug for Dollars {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "{}", self)
}
}
impl Display for Dollars {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
let as_str = format!(
"{}${}.{:02}",
if self.in_cents() < 0 { "-" } else { "" },
self.dollars(),
self.cents(),
);
Display::fmt(&as_str, f)
}
}
impl From<i64> for Dollars {
fn from(cent_value: i64) -> Self {
Self { cent_value }
}
}
impl FromStr for Dollars {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, ParseError> {
if !s.is_ascii() {
return Err(ParseErrorKind::NonAscii.into());
}
let mut chars = s.chars().peekable();
let sign = match chars.peek().copied() {
Some('-') => {
chars.next();
-1
},
c => {
if c == Some('+') {
chars.next();
}
1
},
};
if let Some('$') = chars.peek().copied() {
chars.next();
}
let dollars = chars
.by_ref()
.take_while(|&c| c != '.')
.try_fold(0_i64, |acc, c| {
c.to_digit(10)
.ok_or(ParseErrorKind::InvalidDigit(c))
.and_then(|d| acc.checked_add(d as i64).ok_or(ParseErrorKind::Overflow))
})?;
let cents = match (chars.next(), chars.next()) {
(Some('.'), _) | (_, Some('.')) => return Err(ParseErrorKind::ExtraDecimalPoint.into()),
(Some(_), None) => return Err(ParseErrorKind::BadCentsLength.into()),
(None, _) => 0,
(Some(c1), Some(c2)) => {
let d1 = c1.to_digit(10).ok_or(ParseErrorKind::InvalidDigit(c1))? as i64;
let d2 = c2.to_digit(10).ok_or(ParseErrorKind::InvalidDigit(c1))? as i64;
d1 * 10 + d2
},
};
dollars
.checked_mul(100)
.and_then(|d| d.checked_add(cents))
.and_then(|d| d.checked_mul(sign))
.map(Self::from)
.ok_or(ParseErrorKind::Overflow.into())
}
}
impl Neg for Dollars {
type Output = Self;
fn neg(self) -> Self::Output {
Self {
cent_value: -self.cent_value,
}
}
}
impl Sub for Dollars {
type Output = Self;
fn sub(self, other: Self) -> Self::Output {
Self::from(self.in_cents() - other.in_cents())
}
}
#[derive(Clone, Debug, thiserror::Error)]
#[error("failed to parse dollars: {0}")]
pub struct ParseError(#[from] ParseErrorKind);
#[derive(Clone, Debug, thiserror::Error)]
enum ParseErrorKind {
#[error("invalid digit '{0}'")]
InvalidDigit(char),
#[error("value overflows")]
Overflow,
#[error("cents must be two digits long")]
BadCentsLength,
#[error("too many decimal points")]
ExtraDecimalPoint,
#[error("non-ASCII strings are not allowed")]
NonAscii,
}
#[cfg(test)]
mod tests {
}