diff --git a/README.md b/README.md new file mode 100644 index 0000000..35e6329 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +https://jvns.ca/blog/2023/01/13/examples-of-floating-point-problems/ \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 8be1352..debf40f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,6 +8,11 @@ struct Real { decimal_parts: HashMap } +struct DecimalSubtraction { + carry: bool, + values: HashMap +} + impl Real { pub fn new_int(int_part: i32) -> Real { Real { int_part: int_part, decimal_parts: HashMap::new() } @@ -25,8 +30,10 @@ impl Real { Real { int_part: s.parse::().unwrap(), decimal_parts: HashMap::new() } }, Some(idx) => { - let int_part = s[0..idx].parse::().unwrap(); - dbg!(int_part); + let int_part = match &s[0..1] { + "-" => {-s[1..idx].parse::().unwrap()}, + _ => {s[0..idx].parse::().unwrap()} + }; let decimal_string = &s[idx+1..]; dbg!(decimal_string); @@ -52,6 +59,48 @@ impl Real { } } } + + // Cannot put this inside the `Sub impl` because Rust only allows the implementation of the named methods. + fn subtract_decimals(minuend: HashMap, subtrahend: HashMap) -> DecimalSubtraction { + // default to -1 because the 0th index is the first digit after the decimal place + let highest_index = *max(minuend.keys().max().unwrap_or(&-1), subtrahend.keys().max().unwrap_or(&-1)); + if highest_index == -1 as i32 { + // i.e. if both decimals are empty + return DecimalSubtraction{carry: false, values: HashMap::new()} + } + + let mut subtracted_decimal_parts = HashMap::new(); + let mut should_subtraction_carry_to_integers = false; + + for idx in (0..=highest_index).rev() { + // `decimal_part` added, here, because if a previous (i.e. smaller-denomination) subtraction ended up negative, + // a `-1` will have been put in the `decimal_part` map. + let subtraction = subtracted_decimal_parts.get(&idx).unwrap_or(&0) + minuend.get(&idx).unwrap_or(&0) - subtrahend.get(&idx).unwrap_or(&0); + + match subtraction { + x if 0 < x && x <=9 => { + subtracted_decimal_parts.insert(idx, subtraction); + }, + x if x < 0 => { + subtracted_decimal_parts.insert(idx, 10+subtraction); + if idx > 0 { + // TODO - hmmm. I should probably actually be using unsigned ints for the values. + // This approach would still work, I'd just need a rule that "if there's _any_ value + // in the decimal_part slot, subtract 1", rather than blindly adding the value (which + // is always -1) + subtracted_decimal_parts.insert(idx-1, -1); + } else { + should_subtraction_carry_to_integers = true; + } + }, + _ => { + // x == 0 - so clear the idx-th value + subtracted_decimal_parts.remove(&idx); + } + } + }; + DecimalSubtraction { carry: should_subtraction_carry_to_integers, values: subtracted_decimal_parts } + } } impl Add for Real { @@ -84,11 +133,46 @@ impl Sub for Real { type Output = Real; fn sub(self, other: Real) -> Real { - let int_part = self.int_part - other.int_part; - Real::new_int(int_part) + let int_subtraction = self.int_part - other.int_part; + match int_subtraction { + x if x > 0 => { + let decimal_subtraction = Real::subtract_decimals(self.decimal_parts, other.decimal_parts); + Real::new(int_subtraction - (if decimal_subtraction.carry {1} else {0}), decimal_subtraction.values) + }, + x if x < 0 => { + let inverted = other - self; + Real::new(-inverted.int_part, inverted.decimal_parts) + }, + x if x == 0 => { + // Since integer parts are equal, we have to check case-by-case which is larger + for idx in 0..max(self.decimal_parts.len(), other.decimal_parts.len()) { + // ...just when I think I understand the borrow-checker, this horror-show of casting becomes necessary... + if self.decimal_parts.get(&(idx as i32)).unwrap_or(&(0 as i8)) > other.decimal_parts.get(&(idx as i32)).unwrap_or(&(0 as i8)) { + let decimal_subtraction = Real::subtract_decimals(self.decimal_parts, other.decimal_parts); + // We know that `decimal_subtraction.carry` is false, because the `self.decimal_parts` is larger + return Real::new(0, decimal_subtraction.values); + } + + if self.decimal_parts.get(&(idx as i32)).unwrap_or(&(0 as i8)) < other.decimal_parts.get(&(idx as i32)).unwrap_or(&(0 as i8)) { + return other - self; + } + + // and, implicitly - if they are equal, then continue + } + // Compared all the digits and they're equal, so the original two numbers are equal + Real::new(0, HashMap::new()) + + }, + _ => {panic!("int_subtraction is not greater than, lesser than, or equal to 0, but a secret fourth thing.")} + } + } + + + } + impl PartialEq for Real { fn eq(&self, other: &Real) -> bool { // And also compare decimals @@ -115,6 +199,7 @@ mod tests { assert_eq!(one + two, Real::new_int(3)); } + // TODO - Can I separate these into "test suites" so that each can run as their own function (with, presumably, better reporting?) #[test] fn real_addition() { // Basic equality @@ -159,9 +244,45 @@ mod tests { } #[test] - fn subtraction() { + fn int_subtraction() { let three = Real::new_int(3); let one = Real::new_int(1); assert_eq!(three - one, Real::new_int(2)); } + #[test] + fn real_subtraction() { + // Basic case + assert_eq!( + Real::from_string("3.45") - Real::from_string("1.23"), + Real::from_string("2.22") + ); + + // Negative result + assert_eq!( + Real::from_string("1.23") - Real::from_string("3.45"), + Real::from_string("-2.22") + ); + + // Equality + assert_eq!( + Real::from_string("1.23") - Real::from_string("1.23"), + Real::from_string("0") + ); + + // Carry + assert_eq!( + Real::from_string("1.11") - Real::from_string("1.02"), + Real::from_string("0.09") + ); + + // Carry into integers + assert_eq!( + Real::from_string("1.0") - Real::from_string("0.1"), + Real::from_string("0.9") + ) + + // Multiple carry + } + + // TODO - test for optimization, don't keep unnecessary 0's! } \ No newline at end of file