diff --git a/config.json b/config.json index 564a679c9..dd3f66bfd 100644 --- a/config.json +++ b/config.json @@ -647,6 +647,20 @@ "traits" ] }, + { + "uuid": "7cefed7c-37f4-46c5-9a45-68fe4d0fb326", + "slug": "decimal", + "core": false, + "unlocked_by": null, + "difficulty": 6, + "topics": [ + "struct", + "traits", + "string parsing", + "bigint", + "external crates (optional)" + ] + }, { "uuid": "f3172997-91f5-4941-a76e-91c4b8eed401", "slug": "anagram", diff --git a/exercises/decimal/.gitignore b/exercises/decimal/.gitignore new file mode 100644 index 000000000..cb14a4206 --- /dev/null +++ b/exercises/decimal/.gitignore @@ -0,0 +1,7 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock +Cargo.lock diff --git a/exercises/decimal/.meta/description.md b/exercises/decimal/.meta/description.md new file mode 100644 index 000000000..f6f313f39 --- /dev/null +++ b/exercises/decimal/.meta/description.md @@ -0,0 +1,19 @@ +Implement an arbitrary-precision `Decimal` class. + +Floating point numbers are the most common representation of non-integer real numbers in computing, and they're a common standard defined by [IEEE 754](https://en.wikipedia.org/wiki/IEEE_754). They're very flexible and versatile, but they do have some limitations. Famously, in floating point arithmetic, [`0.1 + 0.2 != 0.3`](http://0.30000000000000004.com/). + +The solution to this issue is to find another, lossless way to model arbitrary-precision non-integer reals. This may be less efficient in terms of memory or processing speed than floating point numbers; the goal is to provide exact results. + +Despite `Decimal` being a custom type, we should still be able to treat them as numbers: the `==`, `<`, `>`, `+`, `-`, and `*` operators should all work as expected on Decimals. For expediency, you are not required to implement division, as arbitrary-precision division can very quickly get out of hand. (How do you represent arbitrary-precision `1/3`?) + +In Rust, the way to get these operations on custom types is to implement the relevant traits for your custom object. In particular, you'll need to implement at least `PartialEq`, `PartialOrd`, `Add`, `Sub`, and `Mul`. Strictly speaking, given that the decimal numbers form a total ordering, you should also implement `Eq` and `Ord`, though those traits are not checked for by these tests. + +# Note + +It would be very easy to implement this exercise by using the [bigdecimal](https://crates.io/crates/bigdecimal) crate. Don't do that; implement this yourself. + +# Hints + +- Instead of implementing arbitrary-precision arithmetic from scratch, consider building your type on top of the [num_bigint](https://crates.io/crates/num-bigint) crate. +- You might be able to [derive](https://doc.rust-lang.org/book/first-edition/traits.html#deriving) some of the required traits. +- `Decimal` is assumed to be a signed type. You do not have to create a separate unsigned type, though you may do so as an implementation detail if you so choose. diff --git a/exercises/decimal/.meta/metadata.yml b/exercises/decimal/.meta/metadata.yml new file mode 100644 index 000000000..5d95d5694 --- /dev/null +++ b/exercises/decimal/.meta/metadata.yml @@ -0,0 +1,3 @@ +--- +blurb: "Implement a Decimal type" +source: "Peter Goodspeed-Niklaus" diff --git a/exercises/decimal/Cargo-example.toml b/exercises/decimal/Cargo-example.toml new file mode 100644 index 000000000..e94bc2dd3 --- /dev/null +++ b/exercises/decimal/Cargo-example.toml @@ -0,0 +1,9 @@ +[package] +name = "decimal" +version = "0.1.0" +authors = ["Peter Goodspeed-Niklaus "] + +[dependencies] +try_opt = "0.1.1" +num-bigint = "0.1.40" +num-traits = "0.1.40" diff --git a/exercises/decimal/Cargo.toml b/exercises/decimal/Cargo.toml new file mode 100644 index 000000000..500e00e4c --- /dev/null +++ b/exercises/decimal/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "decimal" +version = "0.1.0" +authors = ["Peter Goodspeed-Niklaus "] + +[dependencies] diff --git a/exercises/decimal/README.md b/exercises/decimal/README.md new file mode 100644 index 000000000..1644a4b5c --- /dev/null +++ b/exercises/decimal/README.md @@ -0,0 +1,59 @@ +# Decimal + +Implement an arbitrary-precision `Decimal` class. + +Floating point numbers are the most common representation of non-integer real numbers in computing, and they're a common standard defined by [IEEE 754](https://en.wikipedia.org/wiki/IEEE_754). They're very flexible and versatile, but they do have some limitations. Famously, in floating point arithmetic, [`0.1 + 0.2 != 0.3`](http://0.30000000000000004.com/). + +The solution to this issue is to find another, lossless way to model arbitrary-precision non-integer reals. This may be less efficient in terms of memory or processing speed than floating point numbers; the goal is to provide exact results. + +Despite `Decimal` being a custom type, we should still be able to treat them as numbers: the `==`, `<`, `>`, `+`, `-`, and `*` operators should all work as expected on Decimals. For expediency, you are not required to implement division, as arbitrary-precision division can very quickly get out of hand. (How do you represent arbitrary-precision `1/3`?) + +In Rust, the way to get these operations on custom types is to implement the relevant traits for your custom object. In particular, you'll need to implement at least `PartialEq`, `PartialOrd`, `Add`, `Sub`, and `Mul`. Strictly speaking, given that the decimal numbers form a total ordering, you should also implement `Eq` and `Ord`, though those traits are not checked for by these tests. + +# Note + +It would be very easy to implement this exercise by using the [bigdecimal](https://crates.io/crates/bigdecimal) crate. Don't do that; implement this yourself. + +# Hints + +- Instead of implementing arbitrary-precision arithmetic from scratch, consider building your type on top of the [num_bigint](https://crates.io/crates/num-bigint) crate. +- You might be able to [derive](https://doc.rust-lang.org/book/first-edition/traits.html#deriving) some of the required traits. +- `Decimal` is assumed to be a signed type. You do not have to create a separate unsigned type, though you may do so as an implementation detail if you so choose. + +## Rust Installation + +Refer to the [exercism help page][help-page] for Rust installation and learning +resources. + +## Writing the Code + +Execute the tests with: + +```bash +$ cargo test +``` + +All but the first test have been ignored. After you get the first test to +pass, remove the ignore flag (`#[ignore]`) from the next test and get the tests +to pass again. The test file is located in the `tests` directory. You can +also remove the ignore flag from all the tests to get them to run all at once +if you wish. + +Make sure to read the [Crates and Modules](https://doc.rust-lang.org/stable/book/crates-and-modules.html) chapter if you +haven't already, it will help you with organizing your files. + +## Feedback, Issues, Pull Requests + +The [exercism/rust](https://github.com/exercism/rust) repository on GitHub is the home for all of the Rust exercises. If you have feedback about an exercise, or want to help implement new exercises, head over there and create an issue. Members of the [rust track team](https://github.com/orgs/exercism/teams/rust) are happy to help! + +If you want to know more about Exercism, take a look at the [contribution guide](https://github.com/exercism/docs/blob/master/contributing-to-language-tracks/README.md). + +[help-page]: http://exercism.io/languages/rust +[crates-and-modules]: http://doc.rust-lang.org/stable/book/crates-and-modules.html + +## Source + +Peter Goodspeed-Niklaus + +## Submitting Incomplete Solutions +It's possible to submit an incomplete solution so you can see how others have completed the exercise. diff --git a/exercises/decimal/example.rs b/exercises/decimal/example.rs new file mode 100644 index 000000000..8819dbced --- /dev/null +++ b/exercises/decimal/example.rs @@ -0,0 +1,187 @@ +use std::cmp::Ordering; +use std::fmt; +use std::ops::{Add, Sub, Mul}; + +#[macro_use] +extern crate try_opt; + +extern crate num_bigint; +use num_bigint::BigInt; +extern crate num_traits; +use num_traits::pow; + +/// Type implementing arbitrary-precision decimal arithmetic +#[derive(Debug, Eq, Clone)] +pub struct Decimal { + digits: BigInt, + decimal_index: usize, +} + +impl Decimal { + fn new(digits: BigInt, decimal_index: usize) -> Decimal { + let mut value = Decimal { + digits: digits, + decimal_index: decimal_index, + }; + value.reduce(); + value + } + + pub fn try_from(mut input: &str) -> Option { + // clear extraneous whitespace + input = input.trim(); + + // don't bother to trim extraneous zeroes + // leave it to users to manage their own memory + + // now build a representation of the number to parse + let mut digits = String::with_capacity(input.len()); + let mut decimal_index = None; + for ch in input.chars() { + match ch { + '0'...'9' | '-' | '+' => { + digits.push(ch); + if let Some(idx) = decimal_index.as_mut() { + *idx += 1; + } + } + '.' => { + if decimal_index.is_some() { + return None; + } + decimal_index = Some(0) + } + _ => return None, + } + } + Some(Decimal::new( + try_opt!(digits.parse::().ok()), + match decimal_index { + Some(idx) => idx, + None => 0, + }, + )) + } + + /// Add precision to the less-precise value until precisions match + /// + /// Precision, in this case, is defined as the decimal index. + fn equalize_precision(mut one: &mut Decimal, mut two: &mut Decimal) { + fn expand(lower_precision: &mut Decimal, higher_precision: &Decimal) { + let precision_difference = + (higher_precision.decimal_index - lower_precision.decimal_index) as usize; + + lower_precision.digits = &lower_precision.digits * + pow(BigInt::from(10_usize), precision_difference); + lower_precision.decimal_index += precision_difference; + } + if one.decimal_index < two.decimal_index { + expand(&mut one, &two) + } else if one.decimal_index > two.decimal_index { + expand(&mut two, &one) + } + assert_eq!(one.decimal_index, two.decimal_index); + } + + /// Eliminate extraneous trailing zeroes + /// + /// This reduces the decimal index, so that the raw values are easier to parse + fn reduce(&mut self) { + let extra_zeroes = self.digits + .to_string() // produce a decimal representation + .chars() + .rev() // trailing values + .take(self.decimal_index) // not counting past the decimal point + .take_while(|&c| c == '0') // counting only `0` digits + .count(); + self.digits = &self.digits / pow(BigInt::from(10_usize), extra_zeroes); + self.decimal_index -= extra_zeroes; + } +} + +macro_rules! auto_impl_decimal_ops { + ($trait:ident, $func_name:ident, $digits_operation:expr, $index_operation:expr) => { + impl $trait for Decimal { + type Output = Self; + fn $func_name(mut self, mut rhs: Self) -> Self { + Decimal::equalize_precision(&mut self, &mut rhs); + Decimal::new( + $digits_operation(self.digits, rhs.digits), + $index_operation(self.decimal_index, rhs.decimal_index), + ) + } + } + } +} + +auto_impl_decimal_ops!(Add, add, |s, o| s + o, |s, _| s); +auto_impl_decimal_ops!(Sub, sub, |s, o| s - o, |s, _| s); +auto_impl_decimal_ops!(Mul, mul, |s, o| s * o, |s, o| s + o); + +macro_rules! auto_impl_decimal_cow { + ($trait:ident, $func_name:ident, $digits_operation:expr, $return_type:ty) => { + impl $trait for Decimal { + fn $func_name(&self, other: &Self) -> $return_type { + if self.decimal_index == other.decimal_index { + $digits_operation(&self.digits, &other.digits) + } else { + // if we're here, the decimal indexes are unmatched. + // We have to compare equal terms. + // clone both sides so we can modify either of them as necessary. + // not efficient, but whatever. + let mut one = self.clone(); + let mut two = other.clone(); + Decimal::equalize_precision(&mut one, &mut two); + one.$func_name(&two) + } + } + } + } +} + +auto_impl_decimal_cow!(PartialEq, eq, |a, b| a == b, bool); +auto_impl_decimal_cow!(Ord, cmp, |a: &BigInt, b: &BigInt| a.cmp(b), Ordering); + +impl PartialOrd for Decimal { + fn partial_cmp(&self, other: &Decimal) -> Option { + Some(self.cmp(other)) + } +} + +impl fmt::Display for Decimal { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // get a representation of the pure digits, + // left-padded with zeroes + let digits = format!("{:0>width$}", self.digits, width = self.decimal_index); + if self.decimal_index == digits.len() { + write!(f, "0.{}", digits) + } else if self.decimal_index == 0 { + write!(f, "{}", digits) + } else { + let (before_index, after_index) = digits.split_at(digits.len() - self.decimal_index); + write!(f, "{}.{}", before_index, after_index) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_display_temp() { + for test_str in vec!["0", "1", "20", "0.3", "0.04", "50.05", "66.0006", "0.007"] { + println!( + "Decimal representation of \"{}\": {}", + test_str, + Decimal::try_from(test_str).expect("This should always become a decimal") + ); + assert_eq!( + test_str, + Decimal::try_from(test_str) + .expect("This should always become a decimal") + .to_string() + ) + } + } +} diff --git a/exercises/decimal/src/lib.rs b/exercises/decimal/src/lib.rs new file mode 100644 index 000000000..0c2bddc81 --- /dev/null +++ b/exercises/decimal/src/lib.rs @@ -0,0 +1,10 @@ +/// Type implementing arbitrary-precision decimal arithmetic +pub struct Decimal { + // implement your type here +} + +impl Decimal { + pub fn try_from(_: &str) -> Option { + unimplemented!() + } +} diff --git a/exercises/decimal/tests/decimal.rs b/exercises/decimal/tests/decimal.rs new file mode 100644 index 000000000..8ba8b8586 --- /dev/null +++ b/exercises/decimal/tests/decimal.rs @@ -0,0 +1,332 @@ +extern crate decimal; +use decimal::Decimal; + +/// Create a Decimal from a string literal +/// +/// Use only when you _know_ that your value is valid. +fn decimal(input: &str) -> Decimal { + Decimal::try_from(input).expect("That was supposed to be a valid value") +} + +/// Some big and precise values we can use for testing. [0] + [1] == [2] +const BIGS: [&'static str; 3] = [ + "100000000000000000000000000000000000000000000.00000000000000000000000000000000000000001", + "100000000000000000000000000000000000000000000.00000000000000000000000000000000000000002", + "200000000000000000000000000000000000000000000.00000000000000000000000000000000000000003", +]; + +// test simple properties of required operations +#[test] +fn test_eq() { + assert!(decimal("0.0") == decimal("0.0")); + assert!(decimal("1.0") == decimal("1.0")); + for big in BIGS.iter() { + assert!(decimal(big) == decimal(big)); + } +} + +#[test] +#[ignore] +fn test_ne() { + assert!(decimal("0.0") != decimal("1.0")); + assert!(decimal(BIGS[0]) != decimal(BIGS[1])); +} + +#[test] +#[ignore] +fn test_gt() { + for slice_2 in BIGS.windows(2) { + assert!(decimal(slice_2[1]) > decimal(slice_2[0])); + assert!(!(decimal(slice_2[0]) > decimal(slice_2[1]))); + } +} + +#[test] +#[ignore] +fn test_lt() { + for slice_2 in BIGS.windows(2) { + assert!(decimal(slice_2[0]) < decimal(slice_2[1])); + assert!(!(decimal(slice_2[1]) < decimal(slice_2[0]))); + } +} + +#[test] +#[ignore] +fn test_add() { + assert_eq!(decimal("0.1") + decimal("0.2"), decimal("0.3")); + assert_eq!(decimal(BIGS[0]) + decimal(BIGS[1]), decimal(BIGS[2])); + assert_eq!(decimal(BIGS[1]) + decimal(BIGS[0]), decimal(BIGS[2])); +} + +#[test] +#[ignore] +fn test_sub() { + assert_eq!(decimal(BIGS[2]) - decimal(BIGS[1]), decimal(BIGS[0])); + assert_eq!(decimal(BIGS[2]) - decimal(BIGS[0]), decimal(BIGS[1])); +} + +#[test] +#[ignore] +fn test_mul() { + for big in BIGS.iter() { + assert_eq!(decimal(big) * decimal("2"), decimal(big) + decimal(big)); + } +} + +// test identities +#[test] +#[ignore] +fn test_add_id() { + assert_eq!(decimal("1.0") + decimal("0.0"), decimal("1.0")); + assert_eq!(decimal("0.1") + decimal("0.0"), decimal("0.1")); + assert_eq!(decimal("0.0") + decimal("1.0"), decimal("1.0")); + assert_eq!(decimal("0.0") + decimal("0.1"), decimal("0.1")); +} + +#[test] +#[ignore] +fn test_sub_id() { + assert_eq!(decimal("1.0") - decimal("0.0"), decimal("1.0")); + assert_eq!(decimal("0.1") - decimal("0.0"), decimal("0.1")); +} + +#[test] +#[ignore] +fn test_mul_id() { + assert_eq!(decimal("2.1") * decimal("1.0"), decimal("2.1")); + assert_eq!(decimal("1.0") * decimal("2.1"), decimal("2.1")); +} + +#[test] +#[ignore] +fn test_gt_positive_and_zero() { + assert!(decimal("1.0") > decimal("0.0")); + assert!(decimal("0.1") > decimal("0.0")); +} + +#[test] +#[ignore] +fn test_gt_negative_and_zero() { + assert!(decimal("0.0") > decimal("-0.1")); + assert!(decimal("0.0") > decimal("-1.0")); +} + + +// tests of arbitrary precision behavior +#[test] +#[ignore] +fn test_add_uneven_position() { + assert_eq!(decimal("0.1") + decimal("0.02"), decimal("0.12")); +} + +#[test] +#[ignore] +fn test_eq_vary_sig_digits() { + assert!(decimal("0") == decimal("0000000000000.0000000000000000000000")); + assert!(decimal("1") == decimal("00000000000000001.000000000000000000")); +} + +#[test] +#[ignore] +fn test_add_vary_precision() { + assert_eq!( + decimal("100000000000000000000000000000000000000000000") + + decimal("0.00000000000000000000000000000000000000001"), + decimal(BIGS[0]) + ) +} + +#[test] +#[ignore] +fn test_cleanup_precision() { + assert_eq!( + decimal( + "10000000000000000000000000000000000000000000000.999999999999999999999999998", + ) + + decimal( + "10000000000000000000000000000000000000000000000.000000000000000000000000002", + ), + decimal("20000000000000000000000000000000000000000000001") + ) +} + +#[test] +#[ignore] +fn test_gt_varying_positive_precisions() { + assert!(decimal("1.1") > decimal("1.01")); + assert!(decimal("1.01") > decimal("1.0")); + assert!(decimal("1.0") > decimal("0.1")); + assert!(decimal("0.1") > decimal("0.01")); +} + +#[test] +#[ignore] +fn test_gt_positive_and_negative() { + assert!(decimal("1.0") > decimal("-1.0")); + assert!(decimal("1.1") > decimal("-1.1")); + assert!(decimal("0.1") > decimal("-0.1")); +} + +#[test] +#[ignore] +fn test_gt_varying_negative_precisions() { + assert!(decimal("-0.01") > decimal("-0.1")); + assert!(decimal("-0.1") > decimal("-1.0")); + assert!(decimal("-1.0") > decimal("-1.01")); + assert!(decimal("-1.01") > decimal("-1.1")); +} + +// test signed properties +#[test] +#[ignore] +fn test_negatives() { + assert!(Decimal::try_from("-1").is_some()); + assert_eq!(decimal("0") - decimal("1"), decimal("-1")); + assert_eq!(decimal("5.5") + decimal("-6.5"), decimal("-1")); +} + +#[test] +#[ignore] +fn test_explicit_positive() { + assert_eq!(decimal("+1"), decimal("1")); + assert_eq!(decimal("+2.0") - decimal("-0002.0"), decimal("4")); +} + +#[test] +#[ignore] +fn test_multiply_by_negative() { + assert_eq!(decimal("5") * decimal("-0.2"), decimal("-1")); + assert_eq!(decimal("-20") * decimal("-0.2"), decimal("4")); +} + +#[test] +#[ignore] +fn test_simple_partial_cmp() { + assert!(decimal("1.0") < decimal("1.1")); + assert!(decimal("0.00000000000000000000001") > decimal("-20000000000000000000000000000")); +} + +// test carrying rules +// these tests are designed to ensure correctness of implementations for which the +// integer and fractional parts of the number are stored separately +#[test] +#[ignore] +fn test_carry_into_integer() { + assert_eq!(decimal("0.901") + decimal("0.1"), decimal("1.001")) +} + +#[test] +#[ignore] +fn test_carry_into_fractional_with_digits_to_right() { + assert_eq!(decimal("0.0901") + decimal("0.01"), decimal("0.1001")) +} + +#[test] +#[ignore] +fn test_add_carry_over_negative() { + assert_eq!(decimal("-1.99") + decimal("-0.01"), decimal("-2.0")) +} + +#[test] +#[ignore] +fn test_sub_carry_over_negative() { + assert_eq!(decimal("-1.99") - decimal("0.01"), decimal("-2.0")) +} + +#[test] +#[ignore] +fn test_add_carry_over_negative_with_fractional() { + assert_eq!(decimal("-1.99") + decimal("-0.02"), decimal("-2.01")) +} + +#[test] +#[ignore] +fn test_sub_carry_over_negative_with_fractional() { + assert_eq!(decimal("-1.99") - decimal("0.02"), decimal("-2.01")) +} + +#[test] +#[ignore] +fn test_carry_from_rightmost_one() { + assert_eq!(decimal("0.09") + decimal("0.01"), decimal("0.1")) +} + +#[test] +#[ignore] +fn test_carry_from_rightmost_more() { + assert_eq!(decimal("0.099") + decimal("0.001"), decimal("0.1")) +} + +#[test] +#[ignore] +fn test_carry_from_rightmost_into_integer() { + assert_eq!(decimal("0.999") + decimal("0.001"), decimal("1.0")) +} + +// test arithmetic borrow rules +#[test] +#[ignore] +fn test_add_borrow() { + assert_eq!(decimal("0.01") + decimal("-0.0001"), decimal("0.0099")) +} + +#[test] +#[ignore] +fn test_sub_borrow() { + assert_eq!(decimal("0.01") - decimal("0.0001"), decimal("0.0099")) +} + +#[test] +#[ignore] +fn test_add_borrow_integral() { + assert_eq!(decimal("1.0") + decimal("-0.01"), decimal("0.99")) +} + +#[test] +#[ignore] +fn test_sub_borrow_integral() { + assert_eq!(decimal("1.0") - decimal("0.01"), decimal("0.99")) +} + +#[test] +#[ignore] +fn test_add_borrow_integral_zeroes() { + assert_eq!(decimal("1.0") + decimal("-0.99"), decimal("0.01")) +} + +#[test] +#[ignore] +fn test_sub_borrow_integral_zeroes() { + assert_eq!(decimal("1.0") - decimal("0.99"), decimal("0.01")) +} + +#[test] +#[ignore] +fn test_borrow_from_negative() { + assert_eq!(decimal("-1.0") + decimal("0.01"), decimal("-0.99")) +} + +#[test] +#[ignore] +fn test_add_into_fewer_digits() { + assert_eq!(decimal("0.011") + decimal("-0.001"), decimal("0.01")) +} + +// misc tests of arithmetic properties +#[test] +#[ignore] +fn test_sub_into_fewer_digits() { + assert_eq!(decimal("0.011") - decimal("0.001"), decimal("0.01")) +} + +#[test] +#[ignore] +fn test_add_away_decimal() { + assert_eq!(decimal("1.1") + decimal("-0.1"), decimal("1.0")) +} + +#[test] +#[ignore] +fn test_sub_away_decimal() { + assert_eq!(decimal("1.1") - decimal("0.1"), decimal("1.0")) +}