Implement subtraction

This commit is contained in:
Jack Jackson 2023-05-28 18:37:17 -07:00
parent 2aa53c41dc
commit 35659f4a1a
2 changed files with 127 additions and 5 deletions

1
README.md Normal file
View File

@ -0,0 +1 @@
https://jvns.ca/blog/2023/01/13/examples-of-floating-point-problems/

View File

@ -8,6 +8,11 @@ struct Real {
decimal_parts: HashMap<i32, i8>
}
struct DecimalSubtraction {
carry: bool,
values: HashMap<i32, i8>
}
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::<i32>().unwrap(), decimal_parts: HashMap::new() }
},
Some(idx) => {
let int_part = s[0..idx].parse::<i32>().unwrap();
dbg!(int_part);
let int_part = match &s[0..1] {
"-" => {-s[1..idx].parse::<i32>().unwrap()},
_ => {s[0..idx].parse::<i32>().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<i32, i8>, subtrahend: HashMap<i32, i8>) -> 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!
}